How It Works
Before writing any code, understand the three moving parts. This page explains the OAuth2 flow, the package's entry points, and the security model — the concepts every guide builds on.
The three parts
┌─────────────┐ ┌──────────────────┐ ┌────────────┐
│ Browser │ ──────► │ Your backend │ ──────► │ Frappe │
│ (React/Vue/ │ │ (Express/Next/Ni │ │ (OAuth2 │
│ Next/…) │ ◄────── │ tro/Laravel) │ ◄────── │ provider) │
└─────────────┘ └──────────────────┘ └────────────┘
user clicks holds clientSecret, holds user
login button exchanges code, accounts and
creates session login page- Browser — where the user clicks "Login with Frappe". Uses
sso-frappe/browser(or nothing, if the backend owns the redirect). - Your backend — the only place
clientSecretlives. Usessso-frappe/serverto exchange the authorization code for tokens and fetch the user profile. - Frappe — the identity provider. It hosts the login page, issues the code, and serves the user profile.
The flow, step by step
Browser (SPA) Your backend Frappe
───────────── ──────────── ──────
1. startLogin()
state + PKCE → sessionStorage
2. redirect ────────────────────────────────────────────────────────► authorize page
3. user logs in + approves
4. ◄──── code + state ── redirect back to your callback ────────────
5. handleCallback()
validate state
6. POST {code, codeVerifier} ──► 7. exchangeCode(code, verifier)
8. getUserProfile(token) ──► userinfo
9. upsert user (your schema)
10. create session (cookie/JWT)
11. ◄── session ──What each step does:
startLogin()— generates a randomstate(CSRF protection) and a PKCEcode_verifier/code_challengepair, stores them insessionStorage, then redirects the browser to Frappe's authorization page.- Frappe login page — the user authenticates (and approves, if consent is required).
- Redirect back — Frappe appends
?code=...&state=...to yourredirectUri. handleCallback()— parses the URL, validates that the returnedstatematches the stored one, and hands you{ code, codeVerifier }.- POST to backend — your frontend sends the code + verifier to your backend. Never exchange tokens in the browser.
exchangeCode()— backend sendscode+clientSecret+code_verifierto Frappe's token endpoint. Gets back an access token.getUserProfile()— backend fetches the user profile (sub,email,name,roles, ...) with the access token.- Upsert user — your app matches or creates a local user. Recommended: identity schema (
users+user_identities). - Create session — your app sets its own session cookie/JWT. Frappe is no longer involved until the next login.
Entry points
| Import | Environment | What it does |
|---|---|---|
sso-frappe (root) |
Any | Types, config validator (defineFrappeSSOConfig), error classes, validateState. |
sso-frappe/server |
Node.js / Nitro / Bun / Deno | createFrappeSSO — token exchange, userinfo, discovery. Holds clientSecret. |
sso-frappe/browser |
Browser (SPA) | createFrappeSSOBrowser — state + PKCE + redirect + callback validation. No clientSecret. |
sso-frappe/next-auth |
Next.js | frappeNextAuthProvider — a ready Auth.js v5 provider config. |
Security model
clientSecretlives only in your backend. Never inVITE_*,NEXT_PUBLIC_*, or any browser bundle. The browser client doesn't even accept the field.redirectUrimust exactly match the one registered in Frappe — protocol, host, port, path.- State is validated on callback (CSRF protection).
sso-frappe/browserand/serverdo this automatically. - PKCE is always on (
S256). Even if the code is intercepted, it can't be exchanged without the verifier. id_tokenis ignored. Frappe signs it with HS256 which most OIDC libraries reject; the profile comes from the userinfo endpoint instead.
Package vs. your responsibilities
| Concern | Package | Your app |
|---|---|---|
| OAuth URLs, state, PKCE | ✅ | |
| Token exchange, profile normalization | ✅ | |
| Auth.js provider config | ✅ | |
| Users table / migrations | ✅ | |
| Matching/creating users on login | ✅ | |
| Role mapping, active/inactive checks | ✅ | |
| Session management | ✅ | |
| Login button UI | ✅ |
Next: Frappe Provider Setup — create the OAuth client in Frappe first, then pick your framework guide.