@nexefy/auth-client
v1.4.0
Published
Nexefy Auth OAuth 2.1 client SDK with PKCE support
Readme
@nexefy/auth-client
Nexefy Auth OAuth 2.1 client SDK with PKCE support for browser-based applications.
Installation
npm install @nexefy/auth-client
# or
pnpm add @nexefy/auth-clientQuick Start
import { NexefyAuthClient } from '@nexefy/auth-client'
const authClient = new NexefyAuthClient({
authUrl: 'https://auth.nexefy.com',
clientId: 'your-client-id',
redirectUri: window.location.origin + '/auth/callback',
scopes: ['openid', 'profile', 'email'],
})
// Sign in
await authClient.beginAuthentication()
// Handle callback
await authClient.handleCallback()
// Get user
const user = authClient.userInfo
// Check auth status
const isAuthenticated = authClient.isAuthenticated
// Sign out
await authClient.signOut()Documentation
See the full documentation for detailed usage instructions.
Features
- OAuth 2.1 Authorization Code Flow with PKCE
- Automatic token refresh (timer + lifecycle:
visibilitychange,focus, and bfcachepageshowrestore) - Race-free, atomic
signOut()with optional federated (IDP SSO) logout - Type-safe API
- Resource server integration with optional organization context
- Zero dependencies (uses Web Crypto API and native fetch)
Session lifecycle
hydrateSession() — recommended for startup and route guards
isAuthenticated is access-token-only and applies refreshBuffer, so it can read false during the last few minutes of a token's life even though the session is still recoverable via a refresh token. Use hydrateSession() to resolve the real state (it performs a silent refresh when needed):
const status = await authClient.hydrateSession()
// 'authenticated' | 'recoverable' | 'unauthenticated'
if (status === 'unauthenticated') {
await authClient.beginAuthentication()
}hasRecoverableSession is also exposed for synchronous checks (valid access token, or a refresh token is present).
Handling 401s — refresh before logging out
On a 401, attempt a refresh and retry once before treating the user as logged out. Only a terminal invalid_grant is non-recoverable:
authClient.on('tokenRefreshFailed', ({ code, terminal }) => {
if (terminal) {
// refresh token revoked/expired — require interactive login
void authClient.beginAuthentication({ prompt: 'login' })
}
// non-terminal (network / 5xx): safe to retry later
})On a terminal failure the client clears its own stored tokens, so it correctly reports unauthenticated and stops retrying a dead refresh token.
Account switching (sign out + sign in as a different user)
App sign-out clears client tokens only. The IDP keeps its own SSO cookie, so signing in again can silently restore the same user. To switch accounts, either force re-authentication or end the IDP session:
// Option A: force the IDP login screen on next sign-in
await authClient.signOut()
await authClient.beginAuthentication({ prompt: 'login' })
// Option B: federated logout — ends the IDP SSO session if the discovery
// document advertises an end_session_endpoint
await authClient.signOut({ federated: true, postLogoutRedirectUri: window.location.origin })Multi-tab note: a sign-out in one tab clears storage, but another open tab keeps its in-memory session until it next refreshes or reloads.
bfcache (Back-Forward Cache)
The client refreshes the token on pageshow with event.persisted === true, so a tab restored from bfcache (common on mobile / Meta Quest browsers) recovers a valid token automatically. If your callback or login page fires its redirect inside an effect, add a pageshow guard there too so a restored snapshot re-runs the redirect.
Events
on(event, cb) / off(event, cb):
| Event | Payload | When |
|-------|---------|------|
| authenticated | { user } | Sign-in completed |
| tokenRefreshed | { user } | Silent refresh succeeded |
| tokenRefreshFailed | { error, code?, terminal } | Refresh failed (terminal true for invalid_grant) |
| signedOut | — | Sign-out completed |
| error | { message, error } | Flow error |
Using with Resource Servers
If you're making API calls to protected resource servers that require organization context:
import { NexefyAuthClient, BaseResourceServer } from '@nexefy/auth-client'
const authClient = new NexefyAuthClient({
authUrl: 'https://auth.nexefy.com',
clientId: 'your-client-id',
organisationSlug: 'your-org-slug', // Required for resource servers
redirectUri: window.location.origin + '/auth/callback',
})
// Extend BaseResourceServer for your API
class MyApiServer extends BaseResourceServer {
async getData() {
return this.get('/api/data') // Automatically includes organisation_slug
}
}
const api = new MyApiServer('https://api.example.com', authClient, 'your-org-slug')License
MIT
