Appearance
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=createto 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(addsprompt=create) — behind two buttons. - Option 2: on the callback, detect
error=access_denied+error_description=user_not_registeredand redirect the user into theprompt=createflow.
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:
| Method | How the secret travels | Notes |
|---|---|---|
client_secret_basic | Authorization: Basic base64(client_id:client_secret) | The default in most server-side libraries. Percent-encode each half. |
client_secret_post | A client_secret field in the form body | Also 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
| Token | Where |
|---|---|
| Access token | Server-side session. Never send it to the browser. |
| Refresh token | Encrypted 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.