Email + password login, refresh-token rotation, 2FA verification, and revoke endpoints.
Log in with email and password
Authenticates a user by email and password. The response has two terminal shapes:
201 Createdwhen 2FA is OFF β the regular{access_token, refresh_token, user}payload.200 OKwhen 2FA is ON β achallenge_tokento spend onPOST /auth/2fa/verifywithin 5 minutes. No tokens are issued yet.
A 401 invalid email or password is returned for both "wrong password" and "email not registered" β same wording either way, to prevent account enumeration.
The access JWT has a 1-hour TTL and is signed with a secret separate from the dashboard's, so a leak on one surface does not compromise the other. The refresh token (cmrr_...) has a 30-day TTL and is rotated on every successful /auth/refresh call.
Log in with email and password βΊ Request Body
emailLowercased server-side for comparison.
passwordSent in plaintext over TLS only.
Log in with email and password βΊ Responses
2FA on β challenge_token issued. Call /auth/2fa/verify with the TOTP code next.
successcodemessageReturned when the account has 2FA enabled (200 OK). Spend challenge_token on POST /auth/2fa/verify within 5 minutes.
Verify a 2FA challenge
Completes a 2FA login by spending the challenge_token returned by /auth/login plus a TOTP or recovery code. Server tries TOTP first then falls back to recovery codes β the client does not need to specify which one it is sending.
Recovery codes are single-use and hashed at rest. The challenge_token is also single-use (~5 minute TTL); a replayed token returns 401.
Verify a 2FA challenge βΊ Request Body
challenge_tokenThe JWT returned by the 2FA branch of /auth/login.
code6-digit TOTP from the authenticator app, OR an alphanumeric recovery code. Server tries TOTP first then falls back to recovery β the client does not need to specify which.
Verify a 2FA challenge βΊ Responses
Verified β same body shape as /auth/login 2FA-OFF success.
successcodemessageReturned when 2FA is OFF (201 Created).
Exchange refresh token for new access + refresh pair
Trades a refresh token for a fresh access + refresh pair. Rotation is enforced β the spent refresh row is revoked, and the response carries a NEW refresh token. The client MUST replace its stored value with the new one; replaying the previous refresh returns 401.
A leaked refresh token can therefore only be exploited once.
Exchange refresh token for new access + refresh pair βΊ Request Body
refresh_tokenExchange refresh token for new access + refresh pair βΊ Responses
Refreshed β new access and refresh pair issued.
successcodemessageReturned when 2FA is OFF (201 Created).
Revoke refresh token(s)
Revokes a refresh row. The access token itself is not invalidated (JWTs are stateless), but it expires in ~1 hour and the client should drop its local copy on logout.
Idempotent: revoking a row that is already revoked returns 200 with revoked: 0.
With all_devices: true this becomes the Sign out everywhere button β every active refresh row for the user is revoked. The refresh_token field is still required as proof-of-possession even when all_devices is set.
Revoke refresh token(s) βΊ Request Body
refresh_tokenRefresh row to revoke. Even when all_devices=true, the field is required as proof-of-possession.
all_devicesWhen true, revokes every active refresh row for the user β useful for a Sign out everywhere button.
Revoke refresh token(s) βΊ Responses
Revoked. revoked is the count of refresh rows actually marked revoked β 0 means already revoked.
successcodemessage