Skip to content

Web — server-rendered (Next.js / ASP.NET Core / Express)

An app with a backend. Use a confidential client: keep the client_secret server-side and send it at /token.

Pick your path: Follow the guide below, or jump to the AI prompt.

Follow the guide

A — Login vs enrol

  • Login: start the OIDC challenge as usual → /authorize.
  • Enrol: add prompt=create to the authorization request (most libraries expose this as an extra/authorization parameter) → /authorize?prompt=create.

Choose one of:

  • Option 1 (recommended): two server routes — /login (plain) and /signup (adds prompt=create) — behind two buttons.
  • Option 2: on the callback, detect error=access_denied + error_description=user_not_registered and redirect the user into the prompt=create flow.

B — Redirect callback setup

A standard HTTP handler at your registered redirect path (e.g. /auth/callback, or /signin-oidc if you use the ASP.NET Core OIDC middleware default). Verify state against the value you stored server-side before exchanging the code. Nothing platform-specific.

C — Send the client secret on every /token call

Your secret is required on both grants: the code exchange and every refresh. A refresh token on its own is not enough. If you omit the secret on refresh you get 400 with invalid_client.

Standard OIDC libraries do this for you. Check it if you hand-wrote the refresh call.

Either auth method works, so keep your library's default:

MethodHow the secret travelsNotes
client_secret_basicAuthorization: Basic base64(client_id:client_secret)The default in most server-side libraries. Percent-encode each half.
client_secret_postA client_secret field in the form bodyAlso fine.

Use one method per request. Sending both is rejected with invalid_request.

A rejected refresh does not consume your refresh token. So a wrong secret costs you nothing: fix the credential and retry with the same token.

D — Secure token storage

TokenWhere
Access tokenServer-side session. Never send it to the browser.
Refresh tokenEncrypted DB column or server-side encrypted session. Never expose to the browser.

Refresh tokens rotate on every use — see Refresh tokens rotate.

E — Signing the user out early (optional)

To end a session before the refresh token expires, call POST /revoke with the refresh token. This burns the whole token family.

Authenticate that call the same way you authenticate /token: send your client_id and client_secret. You may only revoke tokens issued to your own client. POST /introspect follows the same rule.

Use an AI prompt

Add EntryIdP biometric OIDC login to this server-rendered app (Next.js, ASP.NET Core,
or Express).

EntryIdP is an OpenID Connect provider. Users authenticate ONLY with a face liveness
check — no typed credentials, OTPs, or social logins. Do not add any sign-in form or credential-entry UI.
Issuer: https://idp-test.entryidp.com (use the issuer from my client registration; read
from config/env — never hardcode).

Before writing code:
1. Detect the framework and version from package.json or the .csproj file.
2. Use the framework's standard OIDC integration:
   - Next.js: next-auth with a custom OIDC provider (wellKnown = discovery URL).
   - ASP.NET Core: AddOpenIdConnect with Authority = issuer, ResponseType = "code",
     UsePkce = true, SaveTokens = true, GetClaimsFromUserInfoEndpoint = true.
   - Express: openid-client with Issuer.discover(issuer).
3. Confirm sessions are persisted server-side, not in a plain client cookie.

Implementation:
- Authorization Code + PKCE. code_challenge_method=S256. No implicit flow, never
  response_type=token, and never grant_type=client_credentials.
- This is a CONFIDENTIAL client: client_secret stays server-side only (env var / user-
  secrets / Key Vault). It must never appear in client-rendered code or be committed.
- Send the client_secret on EVERY /token call — the code exchange AND every refresh. A
  refresh token alone is rejected with invalid_client. Either client_secret_basic or
  client_secret_post is accepted, so keep the library default; never send both in one
  request.
- Request scopes: openid profile offline_access (adjust to what was registered).
- Discover all endpoints from {issuer}/.well-known/openid-configuration.
- Store access_token and refresh_token in the server-side session; never send the refresh
  token to the browser.
- Validate the ID token with the library (verify signature via JWKS, iss, aud, exp, nonce).
  Do not decode JWTs without verifying.

Login vs enrol (EntryIdP-specific):
- /login route → plain /authorize (login of an existing face).
- /signup route → same request plus prompt=create (first-time face enrolment).
- On the callback, if error=access_denied and error_description=user_not_registered,
  redirect the user into the /signup (prompt=create) flow.

Refresh handling: on refresh, ALWAYS overwrite the stored refresh_token with the new one
from the response (EntryIdP rotates refresh tokens and burns the family on replay).

Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret in client-side bundles or committed config.
- No client_credentials grant. No implicit flow.
- Endpoints fetched from discovery, not hardcoded.

Done? Run through the pre-launch checklist before you ship, and see Refresh tokens rotate if you requested offline_access.

EntryIdP — Synapser