Appearance
Reference
Endpoint specs, scopes, claims, token lifetimes, error codes, and security rules. For the conceptual overview see How EntryIdP works; for step-by-step integration see Integrate your app.
All endpoints are relative to your issuer URL. Fetch the exact URLs from {issuer}/.well-known/openid-configuration rather than hardcoding them.
Discovery
GET /.well-known/openid-configuration
OIDC provider metadata.
json
{
"issuer": "https://idp-test.entryidp.com",
"authorization_endpoint": "https://idp-test.entryidp.com/authorize",
"token_endpoint": "https://idp-test.entryidp.com/token",
"userinfo_endpoint": "https://idp-test.entryidp.com/userinfo",
"introspection_endpoint": "https://idp-test.entryidp.com/introspect",
"revocation_endpoint": "https://idp-test.entryidp.com/revoke",
"end_session_endpoint": "https://idp-test.entryidp.com/logout",
"jwks_uri": "https://idp-test.entryidp.com/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"id_token_signing_alg_values_supported": ["RS256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
"code_challenge_methods_supported": ["S256"]
}Only authorization_code and refresh_token grants are advertised, and only code responses with S256 PKCE. There is no client_credentials grant and no implicit flow.
GET /.well-known/jwks.json
The public RSA key used to verify token signatures (RS256). Match the JWT kid header to the kid in the key set; refetch when validation fails with an unknown kid.
json
{
"keys": [
{ "kty": "RSA", "use": "sig", "alg": "RS256", "kid": "arn:aws:kms:eu-west-2:…:key/…", "n": "…base64url…", "e": "AQAB" }
]
}kid is the full AWS KMS key ARN that signs tokens — not a short opaque ID. It's stable per signing key and only changes on key rotation; don't assume a short kid format in custom key-lookup logic.
GET /authorize
Starts the flow. Redirect the user's browser here.
| Parameter | Required | Notes |
|---|---|---|
response_type | Yes | Must be code. |
client_id | Yes | Your registered client identifier. |
redirect_uri | Yes | Exact match of a registered URI. |
code_challenge | Yes | PKCE S256 challenge. |
code_challenge_method | No | Defaults to S256; only S256 is accepted. |
scope | Recommended | Space-separated; include openid. |
state | Recommended | Opaque random value, echoed back on redirect. |
nonce | Recommended | Echoed into the ID token nonce claim. |
prompt | No | create opts into first-time enrolment (see login vs enrol). |
client_id, redirect_uri, code_challenge, and response_type are validated at entry; missing any of them returns 400 with invalid_request.
Success: the user is taken through the liveness UI, then redirected to redirect_uri?code=…&state=….
Error redirect (redirect_uri?error=…&error_description=…&state=…):
error | error_description | Condition |
|---|---|---|
access_denied | liveness_check_failed | The liveness check did not pass. |
access_denied | user_not_registered | The live face matched no enrolled user, and the request did not include prompt=create. |
access_denied | account_suspended | The user's account is suspended by an admin. |
Direct 400 errors (malformed request, returned as JSON rather than a redirect):
error | Condition |
|---|---|
invalid_request | Missing required parameter, or code_challenge_method other than S256. |
unsupported_response_type | response_type is not code. |
invalid_client | Unknown client_id. |
POST /token
Content-Type: application/x-www-form-urlencoded. Confidential clients authenticate with client_secret_post (the secret in the form body, bcrypt-verified). Public clients authenticate with PKCE alone.
Authorization Code grant
| Parameter | Required | Notes |
|---|---|---|
grant_type | Yes | authorization_code. |
code | Yes | Code from the callback. |
code_verifier | Yes | PKCE verifier; its S256 hash must equal the original code_challenge. |
client_id | Yes | Your client identifier. |
client_secret | Confidential clients only | Never sent from a browser or mobile app. |
Refresh Token grant
| Parameter | Required | Notes |
|---|---|---|
grant_type | Yes | refresh_token. |
refresh_token | Yes | The most recently issued refresh token. |
client_id | Yes | Your client identifier. |
client_secret | Confidential clients only | — |
Success 200:
json
{
"access_token": "eyJ…",
"id_token": "eyJ…",
"refresh_token": "…",
"token_type": "Bearer",
"expires_in": 900,
"scope": "openid profile offline_access"
}refresh_token is present only when offline_access was granted.
Errors 400:
error | Condition |
|---|---|
invalid_request | code / code_verifier (or refresh_token) missing. |
invalid_grant | Code unknown/expired/already used; PKCE verifier mismatch; or a replayed refresh token (whole family revoked, error_description=refresh token replay detected). |
invalid_client | Confidential client presented a missing or incorrect client_secret. |
unsupported_grant_type | Any grant other than authorization_code or refresh_token — including client_credentials, which is always rejected. |
invalid_grant (account suspended) | The user was suspended; refreshing yields no new tokens. |
GET /userinfo
Returns identity claims for the bearer of a valid access token, filtered by the token's granted scopes.
Authorization: Bearer ACCESS_TOKEN401 if the token is missing, expired, or invalid. See Claims for which claims each scope releases.
GET /logout · POST /logout
OIDC RP-Initiated Logout 1.0. EntryIdP holds no server-side browser session of its own (every login is a fresh liveness check), so logout's job is to validate the request and perform a safe, registered redirect while your app clears its own session.
| Parameter | Required | Notes |
|---|---|---|
id_token_hint | Recommended | A previously issued ID token. Signature and issuer are validated; its expiry is not — an expired ID token is still a valid logout hint. |
client_id | Required if post_logout_redirect_uri is present | Must agree with the id_token_hint audience if both are supplied. |
post_logout_redirect_uri | No | Must exactly match a value registered for the client. |
state | No | Opaque value echoed onto the redirect; must be a URL-safe token. |
- With a valid
post_logout_redirect_uri: redirect to it (withstateappended if supplied). - Without one:
200with{ "message": "You have been signed out." }. - Unregistered or mismatched URI:
400 invalid_request; the user stays on EntryIdP — never an open redirect.
After logout, clear your own session and stored tokens.
POST /revoke · POST /introspect
Content-Type: application/x-www-form-urlencoded, parameters token and optional token_type_hint.
/revoke— always returns200with{}and never reveals whether the token was valid. Revoking a refresh token burns its entire family; revoking an access token adds itsjtito the revocation list./introspect— returns{ "active": true, … }for a live token or{ "active": false }for anything expired, revoked, or unrecognised, with no extra detail.
Scopes
Registered scopes. openid is required on every request; the rest gate data released at /userinfo (or claims in the token).
| Scope | Releases |
|---|---|
openid | OIDC itself — an ID token is issued. (Required.) |
profile | Your name, email address and phone number (self-declared). |
offline_access | A refresh token, to keep the user signed in without re-verifying. |
identity:verified | Your verified identity, date of birth and ID number (from the identity-proofing bureau). |
liveness:result | Reserved — see note below. |
roles | Reserved — see note below. |
Request only what you need. Don't request identity:verified unless your app makes identity-verified decisions, and don't request offline_access from a browser-only SPA.
rolesandliveness:resultdon't currently gate anything. The claims they describe —roles,tenant_id,face_liveness_verified,liveness_confidence_score,acr,amr— are always present in the ID token regardless of which scopes you request (see the "Always" column in the ID token claims table below). Requesting these two scopes has no effect today; they're listed here because they're advertised in discovery, not because they change what you receive.
Claims
Token headers: access tokens use "typ": "at+jwt" (the RFC 9068 JWT access-token profile) — don't assume a bare "JWT" typ. ID tokens use the default "typ": "JWT". Both use the KMS-ARN kid described under Discovery above.
ID token (from /token)
The ID token is a pure authentication assertion — it carries no PII by design. Fetch profile/identity data from /userinfo.
| Claim | Type | Present | Meaning |
|---|---|---|---|
sub | string | Always | Stable user identifier. |
iss | string | Always | Issuer URL. |
aud | string | Always | The client_id the token was issued to. |
iat | integer | Always | Issued-at (Unix time). |
exp | integer | Always | Expiry (Unix time). |
auth_time | integer | Always | When the liveness check completed. |
acr | string | Always | urn:entryidp:biometric:liveness. |
amr | array | Always | ["face"]. |
face_liveness_verified | string | Always | "true" when liveness passed. |
liveness_confidence_score | string | Always | The liveness detection confidence score. |
roles | array | Always | Roles in the client's tenant; empty array if the user is not enrolled there. |
tenant_id | string | When enrolled | Present only when the user holds a membership in the client's tenant. |
nonce | string | When sent | Echo of the request nonce. |
/userinfo by scope
Always: sub.
profile: name, given_name, family_name, gender, email, email_verified, phone_number, phone_number_verified. email_verified and phone_number_verified are always false — EntryIdP does not verify email or phone.
identity:verified: identity_verified (boolean), birthdate, id_number. The latter two are absent until identity verification completes.
id_numbercarries no document-type claim — nothing distinguishes an ID-book number from a smart-card or passport number today, and the underlying verified-identity record doesn't track document type either. Don't build logic that infers document type fromid_number's presence or shape.
face_liveness_verified(ID token) andidentity_verified(/userinfo) are separate. Liveness proves physical presence; identity verification proves a legal-identity document was checked.
Token lifetimes
| Token | Default | Notes |
|---|---|---|
| Authorization code | 60 seconds | Single-use; deleted on exchange. |
| Access token | 15 minutes (expires_in = 900) | Per-client override possible. |
| Refresh token | 30 days | Per-client override possible; rotates on every use; replay burns the family. |
Redirect & post-logout URI rules
- Exact string match only — no wildcards, no prefix matching. Scheme, path, query, and trailing slash must match exactly.
httpsfor all non-localhost web URIs;http://localhostis for local/test use only.- Mobile redirect URIs are custom URL schemes (e.g.
com.yourapp://callback). EntryIdP does not hostapple-app-site-associationorassetlinks.json, so Universal Links / App Links are not supported. - Post-logout redirect URIs follow the same exact-match rule; an unregistered value keeps the user on EntryIdP.
Security checklist
- PKCE is mandatory on every client; only
S256is accepted. Theplainmethod is rejected. - Validate the ID token with an established OIDC library: RS256 signature via the JWKS
kid,issequals your issuer,audcontains yourclient_id,expin the future,noncematches. Rejectalg: none. Don't hand-parse JWTs. state— fresh random value per login attempt, stored server-side (or app state), compared byte-for-byte on callback before the code is exchanged.client_secret— confidential clients only, server-side only. Never in browser/mobile code, never committed.- Token placement — SPAs: access token in memory, no
offline_access. Mobile: OS secure storage (Keychain / Keystore). Backends: server-side session; refresh token encrypted, never sent to the browser. - Identifier — use
subas the stable user key. Do not match users byemailalone (email_verifiedis alwaysfalse). For cross-system matching useidentity_verified+ verifiedbirthdate/id_number. - Codes are single-use — make your callback idempotent so a retried/duplicated
codedoesn't double-exchange.
Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
Redirect back with error=access_denied&error_description=user_not_registered | The face matched no enrolled user and you didn't send prompt=create. Offer a "Create account" flow that adds prompt=create. |
error=access_denied&error_description=liveness_check_failed | The liveness check failed (poor lighting, no live face, replay). Let the user retry. |
error=access_denied&error_description=account_suspended | An admin suspended the account. Out of the app's control. |
invalid_request at /authorize | A required parameter (client_id, redirect_uri, code_challenge, response_type) is missing, or code_challenge_method isn't S256. |
invalid_client at /authorize | The client_id isn't registered for this environment. |
redirect_uri is not registered for this client | The redirect_uri doesn't byte-match a registered value. Exact match — check scheme, path, trailing slash. |
invalid_grant at /token | Code expired (>60s), already used, or the PKCE code_verifier doesn't match the original code_challenge. Exchange promptly and reuse the matching verifier. |
invalid_grant with refresh token replay detected | A used refresh token was replayed; the family is revoked. Always store the newest refresh token and re-authenticate. |
invalid_client at /token (confidential) | Missing or wrong client_secret. |
unsupported_grant_type | You sent a grant other than authorization_code / refresh_token (e.g. client_credentials, which is never supported). |
401 from /userinfo | Access token missing, expired, or invalid. Refresh or re-authenticate. |
| ID token signature won't verify | Select the JWKS key by the JWT kid; refetch jwks.json if the kid is unknown. Algorithm must be RS256. |
IDX10500: Signature validation failed. No security keys were provided... (resource server, .NET) | A pinned Microsoft.IdentityModel.* transitive package version is silently emptying the discovery document's jwks_uri. See Common setup pitfalls for the fix. |
System.InvalidOperationException: No authenticationScheme was specified... (resource server, .NET) | Missing app.UseAuthentication() before app.UseAuthorization(). See Common setup pitfalls. |