Browser client API
Import:
import {
createFrappeSSOBrowser,
BROWSER_STORAGE_KEYS,
} from 'sso-frappe/browser';Security boundary
The browser client accepts no clientSecret and has no exchangeCode or getUserProfile method. It handles only:
- Generate state and PKCE S256
- Store state + verifier in
sessionStorage - Redirect to Frappe
- Parse callback URL
- Validate state
- Return
{ code, codeVerifier }to your backend
Your backend receives those values and uses sso-frappe/server to exchange the code. Never put a Frappe client secret in VITE_*, NEXT_PUBLIC_*, or any browser bundle.
Configuration
const sso = createFrappeSSOBrowser({
baseUrl: 'https://erp.example.com',
clientId: 'public-client-id',
redirectUri: 'https://app.example.com/auth/callback',
scope: ['openid'],
// optional; defaults to Frappe's standard path
authorizationEndpoint:
'https://erp.example.com/api/method/frappe.integrations.oauth2.authorize',
buttonLabel: 'Login with Frappe',
});baseUrl, clientId, and redirectUri are required. PKCE is always enabled in browser mode.
startLogin()
const { state, codeVerifier } = await sso.startLogin();Persists:
frappe_sso_statefrappe_sso_code_verifier
in sessionStorage, then calls window.location.assign(authorizationUrl).
handleCallback(url?)
On callback page:
const { code, codeVerifier, state } = await sso.handleCallback();
await fetch('/api/auth/frappe/callback', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ code, codeVerifier, state }),
});
sso.clear();Pass a URL explicitly in tests or non-browser runtimes:
await sso.handleCallback('https://app.example.com/auth/callback?code=...&state=...');Throws FrappeSSOStateError if code/state/verifier is missing or state mismatches.
clear()
Remove state and PKCE verifier after successful callback or failed/cancelled flow:
sso.clear();Custom storage
Useful for tests, SSR guards, or custom storage:
const sso = createFrappeSSOBrowser({
baseUrl,
clientId,
redirectUri,
storage: {
getItem: (key) => sessionStorage.getItem(key),
setItem: (key, value) => sessionStorage.setItem(key, value),
removeItem: (key) => sessionStorage.removeItem(key),
},
});Do not use a server database or shared storage for browser secrets. sessionStorage is scoped to the tab, which prevents verifier collisions between tabs.