Skip to content

Web — SPA (React / Vue / Svelte) ​

A browser-only app with no backend. Use a public client (no client_secret) secured by PKCE.

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

Follow the guide ​

A — Login vs enrol ​

  • Login: userManager.signinRedirect() → standard /authorize.
  • Enrol: the same call with { extraQueryParams: { prompt: 'create' } } → /authorize?prompt=create.

Choose one of:

  • Option 1 (recommended): render two buttons — "Sign in" (login) and "Create account" (enrol with prompt=create). Simplest, and new users skip a wasted round-trip.
  • Option 2: attempt login first; if the callback returns error=access_denied with error_description=user_not_registered, restart the flow with prompt=create.

B — Redirect callback setup ​

Your framework's router handles the redirect URI like any other route (e.g. /callback). On that route, call your library's signinRedirectCallback() to complete the exchange. No platform-specific work beyond registering the route's full URL as your redirect URI.

C — Secure token storage ​

TokenWhere
Access tokenIn memory only (a JS variable or library state) — never localStorage.
Refresh tokenDo not request offline_access. SPAs cannot store refresh tokens safely; prompt for re-authentication when the access token expires.

Use an AI prompt ​

Add EntryIdP biometric OIDC login to this SPA (React, Vue, or Svelte).

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
it from an env var, e.g. VITE_ENTRYIDP_ISSUER — never hardcode).

Before writing code:
1. Read package.json to confirm the framework and bundler (Vite, CRA, etc.).
2. Check for an existing OIDC library (oidc-client-ts). If none, install oidc-client-ts.

Implementation:
- Authorization Code + PKCE only. code_challenge_method=S256. Never use implicit flow or
  response_type=token.
- This is a browser-only PUBLIC client: no client_secret in any file.
- Do NOT request offline_access — refresh tokens cannot be stored securely in a browser.
- Keep the access token in memory (UserManager state). Never use localStorage.
- Discover endpoints from {issuer}/.well-known/openid-configuration. Do not hardcode URLs.
- Redirect URI from VITE_ENTRYIDP_REDIRECT_URI; it must exactly match the registered URI.

Login vs enrol (EntryIdP-specific):
- Add a "Sign in" button → signinRedirect() (plain /authorize = login of an existing face).
- Add a "Create account" button → signinRedirect with extraQueryParams { prompt: 'create' }
  (first-time face enrolment).
- In the callback, if the URL contains error=access_denied and
  error_description=user_not_registered, send the user to the "Create account" flow.

Files: src/auth/entryidp.ts (UserManager config), an AuthProvider/context, a Callback
route that calls signinRedirectCallback(), Login/Logout buttons, and .env.example.

Guardrails:
- No sign-in form, OTP, or social-login UI.
- No client_secret anywhere.
- No offline_access scope; no refresh tokens.
- 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