sfsso-frappe

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
  1. Browser — where the user clicks "Login with Frappe". Uses sso-frappe/browser (or nothing, if the backend owns the redirect).
  2. Your backend — the only place clientSecret lives. Uses sso-frappe/server to exchange the authorization code for tokens and fetch the user profile.
  3. 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:

  1. startLogin() — generates a random state (CSRF protection) and a PKCE code_verifier/code_challenge pair, stores them in sessionStorage, then redirects the browser to Frappe's authorization page.
  2. Frappe login page — the user authenticates (and approves, if consent is required).
  3. Redirect back — Frappe appends ?code=...&state=... to your redirectUri.
  4. handleCallback() — parses the URL, validates that the returned state matches the stored one, and hands you { code, codeVerifier }.
  5. POST to backend — your frontend sends the code + verifier to your backend. Never exchange tokens in the browser.
  6. exchangeCode() — backend sends code + clientSecret + code_verifier to Frappe's token endpoint. Gets back an access token.
  7. getUserProfile() — backend fetches the user profile (sub, email, name, roles, ...) with the access token.
  8. Upsert user — your app matches or creates a local user. Recommended: identity schema (users + user_identities).
  9. 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

  • clientSecret lives only in your backend. Never in VITE_*, NEXT_PUBLIC_*, or any browser bundle. The browser client doesn't even accept the field.
  • redirectUri must exactly match the one registered in Frappe — protocol, host, port, path.
  • State is validated on callback (CSRF protection). sso-frappe/browser and /server do this automatically.
  • PKCE is always on (S256). Even if the code is intercepted, it can't be exchanged without the verifier.
  • id_token is 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.