Skip to content

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.

ParameterRequiredNotes
response_typeYesMust be code.
client_idYesYour registered client identifier.
redirect_uriYesExact match of a registered URI.
code_challengeYesPKCE S256 challenge.
code_challenge_methodNoDefaults to S256; only S256 is accepted.
scopeRecommendedSpace-separated; include openid.
stateRecommendedOpaque random value, echoed back on redirect.
nonceRecommendedEchoed into the ID token nonce claim.
promptNocreate 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=…):

errorerror_descriptionCondition
access_deniedliveness_check_failedThe liveness check did not pass.
access_denieduser_not_registeredThe live face matched no enrolled user, and the request did not include prompt=create.
access_deniedaccount_suspendedThe user's account is suspended by an admin.

Direct 400 errors (malformed request, returned as JSON rather than a redirect):

errorCondition
invalid_requestMissing required parameter, or code_challenge_method other than S256.
unsupported_response_typeresponse_type is not code.
invalid_clientUnknown 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

ParameterRequiredNotes
grant_typeYesauthorization_code.
codeYesCode from the callback.
code_verifierYesPKCE verifier; its S256 hash must equal the original code_challenge.
client_idYesYour client identifier.
client_secretConfidential clients onlyNever sent from a browser or mobile app.

Refresh Token grant

ParameterRequiredNotes
grant_typeYesrefresh_token.
refresh_tokenYesThe most recently issued refresh token.
client_idYesYour client identifier.
client_secretConfidential 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:

errorCondition
invalid_requestcode / code_verifier (or refresh_token) missing.
invalid_grantCode unknown/expired/already used; PKCE verifier mismatch; or a replayed refresh token (whole family revoked, error_description=refresh token replay detected).
invalid_clientConfidential client presented a missing or incorrect client_secret.
unsupported_grant_typeAny 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_TOKEN

401 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.

ParameterRequiredNotes
id_token_hintRecommendedA previously issued ID token. Signature and issuer are validated; its expiry is not — an expired ID token is still a valid logout hint.
client_idRequired if post_logout_redirect_uri is presentMust agree with the id_token_hint audience if both are supplied.
post_logout_redirect_uriNoMust exactly match a value registered for the client.
stateNoOpaque value echoed onto the redirect; must be a URL-safe token.
  • With a valid post_logout_redirect_uri: redirect to it (with state appended if supplied).
  • Without one: 200 with { "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 returns 200 with {} and never reveals whether the token was valid. Revoking a refresh token burns its entire family; revoking an access token adds its jti to 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).

ScopeReleases
openidOIDC itself — an ID token is issued. (Required.)
profileYour name, email address and phone number (self-declared).
offline_accessA refresh token, to keep the user signed in without re-verifying.
identity:verifiedYour verified identity, date of birth and ID number (from the identity-proofing bureau).
liveness:resultReserved — see note below.
rolesReserved — 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.

roles and liveness:result don'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.

ClaimTypePresentMeaning
substringAlwaysStable user identifier.
issstringAlwaysIssuer URL.
audstringAlwaysThe client_id the token was issued to.
iatintegerAlwaysIssued-at (Unix time).
expintegerAlwaysExpiry (Unix time).
auth_timeintegerAlwaysWhen the liveness check completed.
acrstringAlwaysurn:entryidp:biometric:liveness.
amrarrayAlways["face"].
face_liveness_verifiedstringAlways"true" when liveness passed.
liveness_confidence_scorestringAlwaysThe liveness detection confidence score.
rolesarrayAlwaysRoles in the client's tenant; empty array if the user is not enrolled there.
tenant_idstringWhen enrolledPresent only when the user holds a membership in the client's tenant.
noncestringWhen sentEcho 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_number carries 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 from id_number's presence or shape.

face_liveness_verified (ID token) and identity_verified (/userinfo) are separate. Liveness proves physical presence; identity verification proves a legal-identity document was checked.


Token lifetimes

TokenDefaultNotes
Authorization code60 secondsSingle-use; deleted on exchange.
Access token15 minutes (expires_in = 900)Per-client override possible.
Refresh token30 daysPer-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.
  • https for all non-localhost web URIs; http://localhost is for local/test use only.
  • Mobile redirect URIs are custom URL schemes (e.g. com.yourapp://callback). EntryIdP does not host apple-app-site-association or assetlinks.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 S256 is accepted. The plain method is rejected.
  • Validate the ID token with an established OIDC library: RS256 signature via the JWKS kid, iss equals your issuer, aud contains your client_id, exp in the future, nonce matches. Reject alg: 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 sub as the stable user key. Do not match users by email alone (email_verified is always false). For cross-system matching use identity_verified + verified birthdate / id_number.
  • Codes are single-use — make your callback idempotent so a retried/duplicated code doesn't double-exchange.

Troubleshooting

SymptomLikely cause / fix
Redirect back with error=access_denied&error_description=user_not_registeredThe 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_failedThe liveness check failed (poor lighting, no live face, replay). Let the user retry.
error=access_denied&error_description=account_suspendedAn admin suspended the account. Out of the app's control.
invalid_request at /authorizeA required parameter (client_id, redirect_uri, code_challenge, response_type) is missing, or code_challenge_method isn't S256.
invalid_client at /authorizeThe client_id isn't registered for this environment.
redirect_uri is not registered for this clientThe redirect_uri doesn't byte-match a registered value. Exact match — check scheme, path, trailing slash.
invalid_grant at /tokenCode 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 detectedA 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_typeYou sent a grant other than authorization_code / refresh_token (e.g. client_credentials, which is never supported).
401 from /userinfoAccess token missing, expired, or invalid. Refresh or re-authenticate.
ID token signature won't verifySelect 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.

EntryIdP — Synapser