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.+, =, 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.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
/api/v1/auth/github and /api/v1/auth/github/callback.