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