Security Notes
Read this before deploying. Most of these are handled by the package automatically — but you need to know what not to do.
clientSecret
Never put FRAPPE_SSO_CLIENT_SECRET in:
VITE_*environment variables (Vite inlines them into the browser bundle)NEXT_PUBLIC_*(Next.js inlines them at build time)- Any file served to the browser (
.html,.jsinpublic/) - Client-side code in general
The browser client (sso-frappe/browser) does not accept clientSecret as a config field. If you try to pass it, TypeScript will error.
// ✅ Correct — browser client, no secret
createFrappeSSOBrowser({
baseUrl,
clientId, // public — safe
redirectUri,
});
// ❌ Wrong — server client in browser code
createFrappeSSO({
baseUrl,
clientId,
clientSecret, // this is now in the browser bundle
redirectUri,
});redirectUri
Must exactly match what you registered in Frappe's OAuth2 Settings:
| Registered in Frappe | Your redirectUri |
Match? |
|---|---|---|
https://app.example.com/callback |
https://app.example.com/callback |
✅ |
https://app.example.com/callback |
http://app.example.com/callback |
❌ (protocol) |
https://app.example.com/callback |
https://app.example.com/callback/ |
❌ (trailing slash) |
https://app.example.com/callback |
https://app.example.com/api/auth/callback |
❌ (path) |
A mismatch causes Frappe to reject the authorization request with redirect_uri mismatch.
State (CSRF)
The state parameter prevents CSRF attacks where an attacker tricks the browser into completing a login flow it didn't start.
sso-frappe/browsergenerates and validates state automatically.sso-frappe/serverexportsvalidateState()for backend-managed flows.- If you build your own flow, always validate state before accepting the authorization code.
PKCE
PKCE (Proof Key for Code Exchange) prevents authorization code interception. Even if someone steals the code, they can't exchange it without the code_verifier.
- Always on (
S256) insso-frappe/browser. - Configurable via
usePkceinsso-frappe/server(default:true). - There is no reason to turn it off in production.
HTTPS enforcement
By default, baseUrl and redirectUri must use https://. If you're developing locally with http://localhost:
createFrappeSSOBrowser({
baseUrl: 'http://localhost:8080',
// ...
allowInsecureHttp: true, // local dev only
});Set FRAPPE_SSO_ALLOW_INSECURE_HTTP=true in .env for local dev. Never enable this in production.
Do not derive
allowInsecureHttpfromNODE_ENV !== 'production'— Docker containers setNODE_ENV=productioneven for local dev. Use an explicit env var.
Session creation
Create your app session after you've resolved the local user — not before. The order:
- Exchange code → get access token
- Fetch profile from Frappe
- Match/create local user (see identity schema)
- Check user is active
- Now create the session
If you create the session first and the user lookup fails, you end up with an authenticated session for a non-existent user.
Tokens
The package does not store access/refresh tokens. If you need to call Frappe APIs on behalf of the user later:
- Store tokens in a dedicated table (
frappe_tokens), not inuser_identities. - Encrypt tokens at rest.
- Never put tokens in cookies or localStorage.