Skip to main content

JWT structure

Every AspFox JWT contains these claims: Access tokens expire after 15 minutes. Refresh tokens expire after 7 days.

Registration and email verification

New users cannot log in until they verify their email address.
The verification token is URL-safe base64 encoded — no +, =, or / characters appear in the URL. Tokens expire after 24 hours. If expired, the user can request a new one via POST /api/v1/auth/resend-verification. If an unverified user tries to log in, the response is 400 Bad Request with error code EMAIL_NOT_VERIFIED, not 401. This distinction lets the frontend show an appropriate message with a resend link.

Refresh token rotation with reuse detection

Every time an access token is refreshed, the old refresh token is revoked and a new one is issued. This is called token rotation.
Why reuse detection matters: If an attacker steals a refresh token and uses it before the legitimate user does, the legitimate user’s next refresh attempt will fail — their token was already revoked when the attacker refreshed. At that point, the entire token family is revoked, forcing both the attacker and the legitimate user to re-authenticate. This detects the theft and limits the window of compromise. Token families use the FamilyId field on RefreshToken. Every refresh creates a new token with the same FamilyId. When reuse is detected, a single UPDATE refresh_tokens SET IsRevoked = true WHERE family_id = @familyId revokes the entire chain.

Token storage in the frontend

On page load, the Zustand auth store initializer checks localStorage for a stored refresh token. If found and not expired, it calls POST /api/v1/auth/refresh silently to get a fresh access token. If the refresh fails (revoked, expired), the user is shown the login page. The Axios interceptor handles concurrent 401 responses during token refresh. If three API calls fire simultaneously and all return 401, only one refresh request is sent. The other two are queued and retried with the new access token once it arrives.
localStorage is accessible to JavaScript on the same origin. This is a known tradeoff for developer-facing boilerplate. If your application has stricter security requirements, you can modify the auth flow to use httpOnly cookies for the refresh token. The backend already sets cookies in the response — see AuthController.SetRefreshTokenCookie().

Social login — Google and GitHub OAuth

GitHub OAuth follows the same pattern via /api/v1/auth/github and /api/v1/auth/github/callback.
Magic links expire after 10 minutes. They are single-use. If a user clicks an expired link, they see a clear message and can request a new one.

Admin impersonation

Admins can impersonate any user from the admin panel.
Every impersonation event is written to the audit log with the impersonator’s ID, the target user’s ID, and a timestamp. The impersonation token has the same 15-minute expiry as a normal access token.