sfsso-frappe

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, .js in public/)
  • 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/browser generates and validates state automatically.
  • sso-frappe/server exports validateState() 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) in sso-frappe/browser.
  • Configurable via usePkce in sso-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 allowInsecureHttp from NODE_ENV !== 'production' — Docker containers set NODE_ENV=production even 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:

  1. Exchange code → get access token
  2. Fetch profile from Frappe
  3. Match/create local user (see identity schema)
  4. Check user is active
  5. 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 in user_identities.
  • Encrypt tokens at rest.
  • Never put tokens in cookies or localStorage.