Skip to main content
By the end of this page your backend will send users to Fanvue to authorise, exchange the code they bring back for tokens, and keep those tokens fresh without asking the user to sign in again. It covers the three requests the flow takes, the redirect, the token exchange and the refresh, then client secret management and errors. You need:
  • an app in the Developer Area, with its Client ID and Client Secret
  • the exact redirect URI your callback uses, added on the app’s Authentication tab, and the scopes you need ticked there
  • a server, because the token exchange carries your client secret, and somewhere server-side to keep a session
If you’d rather not write the flow, the Builder SDK implements it, and Build your first app runs it once by hand with curl. The authentication overview explains why each step exists.

The flow at a glance

Your app generates a code verifier and challenge, sends the user to Fanvue to authorise, receives a one-time code on the callback, then exchanges that code plus the verifier for access and refresh tokens.

Generate the PKCE verifier and challenge

PKCE (Proof Key for Code Exchange) is required on every flow. An attacker who steals the authorisation code cannot exchange it for tokens without your original code_verifier.
  1. Generate a code verifier: a cryptographically random string of 43 to 128 characters.
  2. Create a code challenge: SHA-256 hash the verifier, then Base64URL-encode the digest.
  3. Send the challenge: include code_challenge and code_challenge_method=S256 in the authorisation request.
  4. Store the verifier until the token exchange. See Storing the code verifier.
  5. Send the verifier: include code_verifier when exchanging the code.
  6. Server verification: Fanvue checks that SHA256(code_verifier) matches the code_challenge it received.

Storing the code verifier

Keep the code_verifier between the authorisation request and the token exchange in one of these places, in order of preference:
  • Server-side session storage (preferred): a session store backed by Redis or a database.
  • Secure HTTP-only cookies: set the Secure, HttpOnly and SameSite=Lax flags.
  • Encrypted client storage: only if server-side storage is not possible.
Never store the verifier in local storage, session storage, URL parameters or client-side JavaScript variables. Scripts injected through XSS can read those locations, and a stolen verifier plus an intercepted code yields tokens. If the verifier is missing on the callback, abandon the attempt and start the flow again.

Generating verifier and challenge

Create 32 random bytes for the verifier and Base64URL-encode them: standard Base64 with + replaced by -, / replaced by _ and trailing = padding removed. The result is at least 43 characters. The challenge is the SHA-256 hash of the verifier, Base64URL-encoded the same way, sent with code_challenge_method=S256.

Build the authorisation URL

Redirect the user to https://auth.fanvue.com/oauth2/auth with the parameters below. Always include the three system scopes alongside your own:
  • openid enables OpenID Connect and the id_token.
  • offline_access returns a refresh token so your app can obtain new access tokens without the user present.
  • offline enables long-term access for background work.

Token exchange

When the authorisation code arrives on your callback, exchange it for access and refresh tokens.

How to authenticate the token request

Fanvue registers every app as a confidential client with the client_secret_basic authentication method, so your client_id and client_secret go in an HTTP Basic Auth header, not in the request body:
Do not put client_id and client_secret in the POST body. That is the client_secret_post method, which Fanvue clients are not registered for, so the token endpoint rejects the request with invalid_client. Several common libraries default to client_secret_post. Set them to client_secret_basic:
  • openid-client (Node): token_endpoint_auth_method: 'client_secret_basic'
  • Python requests: pass auth=HTTPBasicAuth(client_id, client_secret) and drop the credentials from data
  • Postman / manual: use the Authorization tab’s Basic Auth fields, not body params
Required parameters in the request body:
Response:
expires_in is the access token lifetime in seconds for this response; read it rather than assuming a fixed lifetime. scope lists what was granted, which can be fewer scopes than you requested when the account type cannot use some of them.

Handling the callback

Fanvue redirects the user back to your redirect_uri with the authorisation code and state:
On the callback:
  1. Validate state against the value you stored. A mismatch means the request did not originate from your app; reject it.
  2. Retrieve the stored code_verifier from the session or cookie.
  3. Exchange the code for tokens as shown in Token exchange.
  4. Delete the temporary state and code_verifier.
  5. Store the tokens encrypted at rest. A database is preferred over a plain session.
The example uses Express.js, but the same two routes, initiate then callback, apply to any framework. Persist code_verifier and state when you build the authorisation URL, then verify and exchange them on the callback.

Token refresh

Access tokens are short-lived. Use the refresh token to obtain a new access token without sending the user back through authorisation. Authenticate the refresh request the same way as the token exchange: client_secret_basic via the Basic Auth header, with only grant_type and refresh_token in the body.
Response:
The response returns a new access token and a new refresh token. Persist both before you do anything else with them. Refresh tokens are single-use and rotate on every refresh, even when you re-present one inside the grace window. Refresh tokens rotate and are single-use covers the grace window, reuse detection and what to do when a refresh response is lost.

With the Builder SDK

The Builder SDK implements this whole flow, so you can skip the hand-written client below:
  • createAuthorizationUrl builds the authorise URL and returns the codeVerifier and state to store.
  • exchangeCodeForToken and refreshAccessToken call the token endpoint and return a Result instead of throwing.
  • For a cookie-based off-platform session, getAuthenticatedClient reads the session, refreshes the token when it is close to expiry and returns an API client or null. See Off-platform apps.
  • For apps with several workers, createTokenStoreContext, storeTokenSet and getAccessToken give you an encrypted, compare-and-swap token store that refreshes once per subject. See Storage and security.

By hand

This client refreshes before expiry with a buffer, serialises concurrent refreshes through one promise, and retries once on a 401.
Refresh rules:
  • Refresh before expiry, with a buffer of 5 to 10 minutes.
  • Serialise refreshes for one chain with a lock or shared promise.
  • Retry a 401 once after refreshing. A second 401 is terminal: send the user through authorisation again.
  • Store refresh tokens encrypted and update both tokens from every response.
  • Send the user back through authorisation when a refresh fails with invalid_grant.

Managing your OAuth client secret

When you click Create app in the Developer Area, the dialog that opens shows a Client ID and a Client Secret. The Client ID is a public identifier. The Client Secret authenticates your backend to the token endpoint.

Save the secret immediately

The dialog shows the Client Secret once. Fanvue does not store it in a retrievable form, so if you close the dialog without copying it, your only option is Reset secret.
  • Copy the secret into your secrets manager before you close the dialog.
  • In a local setup, put the secret in .env.local as OAUTH_CLIENT_SECRET. Never commit .env files to version control.

Store it only on your server

The Client Secret is a server-side credential. Never:
  • Commit it to a git repository, public or private.
  • Ship it in a browser bundle or mobile app binary.
  • Write it to application logs, analytics events or error reports.
  • Paste it into shared chat channels or tickets.
Run every token exchange from your backend, which reads the secret from an environment variable or secrets manager at runtime.

Resetting the secret is a breaking change

You can reset the Client Secret at any time with Reset secret on the Authentication tab. The confirmation warns that the current secret is deleted immediately and that the reset can’t be undone. A reset:
  • Stops the old secret working at once. Any running service that still holds the old value receives invalid_client from the token endpoint.
  • Breaks every installation that depends on your backend until each instance holds the new secret.
  • Leaves the Client ID and existing access and refresh tokens unchanged, but your backend cannot refresh tokens until it has the new secret.
Plan the rollout before you reset. Between the reset and the last instance picking up the new value, token exchanges and refreshes fail. For a short rollout window:
  1. Click Reset secret and copy the new secret into your secrets manager.
  2. Roll the new secret out to every instance of your service.
  3. Confirm that a token exchange and a refresh succeed with the new secret before you treat the rotation as complete.
If a secret is exposed, for example committed to a repository or written to a log, treat it as compromised and reset it at once, even if the exposure was brief. Listing requirements sets the security baseline a listed app must meet.

Error handling

The token and authorisation endpoints return standard OAuth errors as JSON with error and error_description fields. Handle them separately from network errors. Retry only on server errors (5xx) and rate limits (429), with exponential backoff, and never on other 4xx responses. Show users a plain message rather than the raw error code. Error responses covers the API’s own errors.

Best practices

Security

  • PKCE is mandatory on every flow.
  • Code verifier: 43 to 128 characters, cryptographically random, stored server-side or in an HTTP-only cookie. Never in URLs, local storage or client-side JavaScript.
  • Use HTTPS in production.
  • Store tokens encrypted at rest.
  • Use a state parameter of at least 32 random characters and verify it on the callback.
  • Register the exact redirect URIs you use on the Authentication tab or in oauth.redirectUris.
  • Keep the Client Secret on your server.

User experience

  • Explain which scopes your app needs and why before the consent screen.
  • Show a plain message on authorisation errors and offer a retry.
  • Give users a way to disconnect your app.

Token management

  • Read expires_in and refresh before expiry.
  • Handle a 401 by refreshing once, then retrying. Treat a second 401 as terminal and send the user through authorisation again.
  • Discard stored tokens when a user disconnects.