Frontend Integration¶
This guide walks through implementing authentication flows in your frontend application. The steps are framework-agnostic and apply to React, Next.js, Vue, Flutter, mobile apps, or any HTTP client.
Token management¶
Storing tokens¶
After login, the server returns an access token and a refresh token. How you store them depends on your setup:
Bearer token (default): the tokens are returned in the JSON response body. Your app stores them and attaches the access token to every request as an Authorization: Bearer <token> header. Common storage options:
- In-memory (a variable or state) - safest against XSS, but lost on page refresh. Good for SPAs that don't need persistence across tabs.
- sessionStorage - persists within the tab, cleared when the tab closes. Accessible to JavaScript on the same origin.
- localStorage - persists across tabs and browser restarts. Accessible to JavaScript on the same origin. Convenient but more exposed to XSS.
Cookie backend: if you configure CookieBackend, the server sets an HttpOnly cookie automatically. Your app doesn't need to store or attach the token manually - the browser handles it. This is the simplest option but requires CSRF protection (see below).
Refreshing tokens¶
Access tokens are short-lived (default 30 minutes). When one expires, use the refresh token to get a new pair:
- Detect a 401 response from any API call
- Call
POST /api/v1/auth/refreshwith the refresh token in the body - Store the new access token (and new refresh token, if rotation is enabled)
- Retry the original request with the new access token
Most HTTP clients support interceptors or middleware that can handle this transparently. The key is to avoid refreshing multiple times concurrently - if several requests fail at once, queue them and refresh only once.
When rotation is enabled (the default), each refresh call invalidates the old refresh token and returns a new one. Always store the latest refresh token. If you use a stale one, the server revokes the entire session.
Logging out¶
Call POST /api/v1/auth/logout with the access token. Optionally include the refresh token in the body to revoke the entire session:
After logout, clear the stored tokens on the client side.
CSRF protection (cookie auth only)¶
If you use CookieBackend with CSRFMiddleware, the server sets a fullauth_csrf cookie on GET requests. On state-changing requests (POST, PUT, DELETE, PATCH), your app must:
- Read the
fullauth_csrfcookie value - Send it as the
X-CSRF-Tokenheader
This is only needed for cookie-based auth. Bearer token auth is not vulnerable to CSRF because the token must be explicitly attached by your code.
OAuth2 login flow¶
OAuth login is a three-step redirect flow between your frontend, your backend, and the OAuth provider (Google, GitHub, etc.).
Step 1: Start the flow¶
Your frontend calls your backend to get the provider's authorization URL:
The redirect_uri tells the provider where to send the user after they authenticate. It must match one of the URIs configured on both the provider's dashboard and in your FullAuthConfig.
The backend returns a JSON response with the authorization_url. Redirect the user to this URL.
Step 2: User authenticates with the provider¶
The user is now on Google's (or GitHub's) login page. After they authenticate, the provider redirects them back to your redirect_uri with two query parameters:
codeis a one-time authorization codestateis a CSRF token your backend generated in Step 1
Step 3: Exchange the code for tokens¶
Your frontend reads code and state from the URL and sends them to your backend:
The backend validates the state, exchanges the code with the provider, creates or links the user account, and returns a JWT token pair. From here, the session works like a normal login.
Handling the redirect page¶
Your frontend's callback page (e.g. /auth/callback) needs to:
- Extract
codeandstatefrom the URL query parameters - Send them to the backend callback endpoint
- Store the returned tokens
- Redirect the user to the app (e.g. dashboard)
If the callback fails (expired state, invalid code), show an error and let the user retry.
Listing available providers¶
Call GET /api/v1/auth/oauth/providers to get the list of configured providers. Use this to dynamically render login buttons (e.g. "Sign in with Google", "Sign in with GitHub").
Passkey (WebAuthn) flow¶
Passkeys use the browser's WebAuthn API. The flow involves passing binary data between your backend and the browser's navigator.credentials API, with base64url encoding in between.
Registration (adding a passkey to an existing account)¶
The user must already be logged in. Registration is a two-step challenge-response:
Step 1: Get registration options
Call POST /api/v1/auth/passkeys/register/begin with the user's access token. The backend returns WebAuthn creation options including a challenge_key and a challenge.
Step 2: Prompt the user
Pass the options to the browser's navigator.credentials.create() API. The browser prompts the user for biometrics (fingerprint, Face ID) or a PIN. A few fields need to be converted from base64url strings to ArrayBuffer before passing to the browser API:
options.challenge- the server's challengeoptions.user.id- the user identifier
Step 3: Send the result back
The browser returns a credential object. Convert its binary fields (rawId, attestationObject, clientDataJSON, transports) to base64url strings and send them to POST /api/v1/auth/passkeys/register/complete along with the challenge_key and a device_name.
The backend verifies the attestation and stores the credential.
Authentication (logging in with a passkey)¶
Authentication is passwordless and does not require an existing session:
Step 1: Get authentication options
Call POST /api/v1/auth/passkeys/authenticate/begin. You can optionally pass an email to narrow the allowed credentials, or omit it for discoverable credentials (the browser shows all available passkeys).
Step 2: Prompt the user
Pass the options to navigator.credentials.get(). Convert challenge and each allowCredentials[].id from base64url to ArrayBuffer first. The browser prompts for biometrics.
Step 3: Send the result back
Convert the credential's binary fields (authenticatorData, clientDataJSON, signature, userHandle) to base64url strings and send them to POST /api/v1/auth/passkeys/authenticate/complete with the challenge_key.
The backend verifies the assertion and returns a JWT token pair, just like a normal login.
Base64url encoding¶
WebAuthn uses ArrayBuffer for binary data, but JSON can't carry binary. The convention is base64url encoding (base64 with - instead of +, _ instead of /, and no padding =).
Your frontend needs two helper functions:
- base64url to ArrayBuffer: decode the string from the server before passing to the browser API
- ArrayBuffer to base64url: encode the browser's response before sending to the server
Most languages and frameworks have libraries for this. Search for "base64url" or "WebAuthn helpers" in your ecosystem.
Conditional UI (autofill)¶
Modern browsers support conditional UI, where passkeys appear in the autofill dropdown alongside passwords. To enable this:
- Call
POST /api/v1/auth/passkeys/authenticate/beginwithout an email (enables discoverable credentials) - Pass
mediation: "conditional"tonavigator.credentials.get() - Add
autocomplete="webauthn"to your username input field
The browser shows available passkeys in the autofill menu. The user selects one and authenticates without clicking a separate button.
Managing passkeys¶
- List passkeys:
GET /api/v1/auth/passkeysreturns the user's registered passkeys with device name, transport type, and last-used timestamp. Show this in a settings page so users can see and manage their passkeys. - Delete a passkey:
DELETE /api/v1/auth/passkeys/{id}removes a passkey. Confirm with the user before deleting.
Email verification flow¶
- After registration, call
POST /api/v1/auth/verify-email/requestwith the user's access token. The backend fires thesend_verification_emailhook with a token. - Your email contains a link to your frontend (e.g.
https://myapp.com/verify?token=eyJ...). - Your frontend's verify page extracts the token from the URL and calls
POST /api/v1/auth/verify-email/confirmwith{"token": "eyJ..."}. - On success, the user's
is_verifiedflag is set totrue.
Password reset flow¶
- User clicks "Forgot password" and enters their email. Your frontend calls
POST /api/v1/auth/password-reset/requestwith{"email": "user@example.com"}. - The backend always returns 202 (regardless of whether the email exists) and fires the
send_password_reset_emailhook. - Your email contains a link (e.g.
https://myapp.com/reset-password?token=eyJ...). - Your frontend's reset page extracts the token, shows a new password form, and calls
POST /api/v1/auth/password-reset/confirmwith{"token": "eyJ...", "new_password": "..."}. - On success, all sessions are revoked. The user must log in again with their new password.