Browser SDK for Authrim - a modern, developer-friendly Identity Provider.
@authrim/web is a browser-specific authentication SDK that provides:
- Unified
{ data, error }response pattern - Type-safe error handling with discriminated unions - Direct Auth - Passkey (WebAuthn), Email Code, Social Login
- OAuth/OIDC - Popup, Silent Auth, Redirect flows
- Session Management - check_session_iframe, Session Monitor, Front-Channel Logout
- Device Flow UI - Helper for CLI/TV/IoT authentication
This package uses @authrim/core internally and provides browser-specific implementations.
npm install @authrim/web
# or
pnpm add @authrim/web
# or
yarn add @authrim/webNote:
@authrim/coreis included as a dependency and will be installed automatically.
The UMD build includes @authrim/core bundled, so no additional scripts are needed.
<!-- Latest version (includes @authrim/core) -->
<script src="https://unpkg.com/@authrim/web/dist/authrim-web.umd.global.js"></script>
<!-- Or via jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@authrim/web/dist/authrim-web.umd.global.js"></script>
<script>
// Global variable: AuthrimWeb
(async () => {
const auth = await AuthrimWeb.createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
});
document.getElementById('login').onclick = async () => {
const { data, error } = await auth.passkey.login();
if (error) {
alert(error.message);
return;
}
console.log('Logged in:', data.user);
};
})();
</script><script type="module">
import { createAuthrim } from 'https://unpkg.com/@authrim/web/dist/index.js';
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
});
</script>import { createAuthrim } from '@authrim/web';
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
});
// Passkey login
const { data, error } = await auth.passkey.login();
if (error) {
console.error('Login failed:', error.message);
return;
}
console.log('Welcome!', data.user.email);
// Sign out
await auth.signOut();const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
enableOAuth: true, // Enable OAuth namespace
});
// OAuth popup login
const { data, error } = await auth.oauth.popup.login({
scopes: ['openid', 'profile', 'email'],
});
if (data) {
console.log('Access Token:', data.accessToken);
}| Category | Feature | Description |
|---|---|---|
| Direct Auth | ||
| Passkey (WebAuthn) | Login, SignUp, Register | |
| Passkey Conditional UI | Autofill integration | |
| Email Code (OTP) | Send and verify codes | |
| Social Login | Popup and redirect flows | |
| OAuth/OIDC | ||
| Authorization Code + PKCE | Standard secure flow | |
| Silent Auth | Hidden iframe session renewal | |
| Popup Auth | Popup window flow | |
| Redirect Auth | Full page redirect flow | |
| Session Management | ||
| CheckSessionIframeManager | postMessage-based session check | |
| SessionMonitor | Periodic session polling with events | |
| FrontChannelLogoutHandler | Handle logout requests from OP | |
| Device Flow | ||
| DeviceFlowUI | Events, countdown, QR code helpers | |
| formatUserCode | Format user code for display | |
| getDeviceFlowQRCodeUrl | Get URL for QR code | |
| Utilities | ||
| Event System | Auth lifecycle events | |
| Response Pattern | Unified { data, error } format |
@authrim/web uses @authrim/core internally. For advanced use cases, you can import from both:
import { createAuthrim, SessionMonitor, FrontChannelLogoutHandler } from '@authrim/web';
import {
// JWT Utilities
decodeJwt,
decodeIdToken,
isJwtExpired,
getIdTokenNonce,
// Base64url
base64urlEncode,
base64urlDecode,
stringToBase64url,
base64urlToString,
// Security
timingSafeEqual,
// Session Management (server-side helpers)
SessionStateCalculator,
FrontChannelLogoutUrlBuilder,
BackChannelLogoutValidator,
BACKCHANNEL_LOGOUT_EVENT,
// Types
type TokenSet,
type OIDCDiscoveryDocument,
type UserInfo,
} from '@authrim/core';
// Use web SDK for browser auth
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
});
// Use core utilities
const decoded = decodeIdToken(idToken);
const isExpired = isJwtExpired(accessToken);| Use Case | Package |
|---|---|
| Browser SPA/PWA | @authrim/web |
| Server-side Node.js | @authrim/core with custom providers |
| Cloudflare Workers | @authrim/core with Workers providers |
| Custom platforms | @authrim/core with your own providers |
| Utility functions only | @authrim/core |
interface AuthrimConfig {
/** Authrim IdP URL */
issuer: string;
/** OAuth client ID */
clientId: string;
/** Enable OAuth/OIDC features (default: false) */
enableOAuth?: boolean;
/** Storage configuration */
storage?: {
type?: 'localStorage' | 'sessionStorage' | 'memory';
prefix?: string;
};
}// Login with passkey
const { data, error } = await auth.passkey.login();
// Login with conditional UI (autofill)
if (await auth.passkey.isConditionalUIAvailable()) {
const { data, error } = await auth.passkey.login({ mediation: 'conditional' });
}
// Sign up with passkey
const { data, error } = await auth.passkey.signUp({
email: 'user@example.com',
displayName: 'John Doe',
});
// Register new passkey (user must be logged in)
const { data, error } = await auth.passkey.register();
// Check support
const isSupported = auth.passkey.isSupported();
const canAutoFill = await auth.passkey.isConditionalUIAvailable();
// Cancel conditional UI
auth.passkey.cancelConditionalUI();// Send verification code
const { data, error } = await auth.emailCode.send('user@example.com', {
type: 'login', // 'login' | 'signup' | 'verification'
});
if (data) {
console.log('Code sent! Expires in', data.expiresIn, 'seconds');
}
// Verify code and authenticate
const { data, error } = await auth.emailCode.verify('user@example.com', '123456');
// Check pending verification
const hasPending = auth.emailCode.hasPendingVerification('user@example.com');
const remainingTime = auth.emailCode.getRemainingTime('user@example.com');
// Clear pending state
auth.emailCode.clearPendingVerification('user@example.com');// Popup login (stays on current page)
const { data, error } = await auth.social.loginWithPopup('google');
if (error && error.error === 'popup_blocked') {
// Fall back to redirect
await auth.social.loginWithRedirect('google');
}
// Redirect login
await auth.social.loginWithRedirect('github', {
redirectUri: 'https://app.example.com/callback',
});
// Handle callback (on callback page)
if (auth.social.hasCallbackParams()) {
const { data, error } = await auth.social.handleCallback();
if (data) {
router.navigate('/dashboard');
}
}
// Get supported providers
const providers = auth.social.getSupportedProviders();
// ['google', 'github', 'apple', 'microsoft', 'facebook', 'twitter']// Get current session
const { data } = await auth.session.get();
if (data) {
console.log('User:', data.user);
console.log('Session expires:', data.session.expiresAt);
}
// Validate session with server
const isValid = await auth.session.validate();
// Refresh session
const session = await auth.session.refresh();
// Check authentication status
const isAuth = await auth.session.isAuthenticated();
// Clear cache
auth.session.clearCache();
// Sign out
await auth.signOut();
// Sign out with redirect
await auth.signOut({ redirectUri: 'https://example.com' });const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
enableOAuth: true,
});
// Build authorization URL manually
const { url, state, nonce } = await auth.oauth.buildAuthorizationUrl({
redirectUri: 'https://app.example.com/callback',
scopes: ['openid', 'profile', 'email'],
});
// Handle callback
const { data, error } = await auth.oauth.handleCallback(window.location.href);
// Silent auth (iframe)
const { data, error } = await auth.oauth.silentAuth.check({
redirectUri: 'https://app.example.com/silent-callback',
timeoutMs: 5000,
});
// Popup login
const { data, error } = await auth.oauth.popup.login({
scopes: ['openid', 'profile'],
popupFeatures: { width: 500, height: 600 },
});Safari ITP や Chrome Third-Party Cookie Phaseout に対応した クロスドメイン SSO を実行できます。
iframe ベースの silent auth は Cookie がブロックされる環境では動作しないため、トップレベル遷移(prompt=none)を使用します。
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
enableOAuth: true,
});
// 1. トップページで SSO 試行(初回のみ)
if (!(await auth.session.isAuthenticated())) {
const ssoAttempted = sessionStorage.getItem('sso_attempted');
if (!ssoAttempted) {
sessionStorage.setItem('sso_attempted', 'true');
// トップレベル遷移で IdP へ prompt=none リダイレクト
// - IdP にセッションあり → SSO 成功 → コールバック → 元のページへ
// - IdP にセッションなし → sso_error=login_required で戻る
await auth.oauth.trySilentLogin({
onLoginRequired: 'return', // 未ログインなら元のページへ戻る
returnTo: window.location.href,
});
return; // リダイレクトするのでここには到達しない
}
// SSO 試行済みだがログインなし → ログインボタン表示
showLoginButton();
}callback.html で必ず handleSilentCallback() を呼ぶ:
// callback.html または callback.ts
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'your-client-id',
enableOAuth: true,
});
// Silent Login のコールバック処理(重要!)
const result = await auth.oauth.handleSilentCallback();
if (result.status === 'error' && result.error === 'not_silent_login') {
// Silent Login ではない → 通常の OAuth コールバック処理へ
if (auth.social.hasCallbackParams()) {
const { data, error } = await auth.social.handleCallback();
// ...
}
}TrySilentLoginOptions:
| Option | Type | Default | Description |
|---|---|---|---|
onLoginRequired |
'return' | 'login' |
'return' |
IdP にセッションがない場合の動作 |
returnTo |
string |
現在のURL | 成功時・return時の戻り先URL |
scope |
string |
- | 追加の OAuth スコープ |
SilentLoginResult:
| Status | Description |
|---|---|
success |
SSO 成功、セッション確立済み |
login_required |
IdP にセッションなし(return 選択時) |
error |
その他のエラー(error, errorDescription を参照) |
⚠️ 重要:handleSilentCallback()を呼ばないと Silent SSO が正しく動作しません。
Manages OP's check_session_iframe for session state monitoring per OIDC Session Management 1.0.
import { CheckSessionIframeManager } from '@authrim/web';
const manager = new CheckSessionIframeManager({
checkSessionIframeUrl: 'https://auth.example.com/connect/checksession',
clientId: 'your-client-id',
opOrigin: 'https://auth.example.com',
timeout: 5000, // optional, default: 5000ms
});
// Initialize iframe
await manager.initialize();
// Check session state
const result = await manager.checkSession(sessionState);
switch (result.response) {
case 'changed':
// Session has changed, re-authenticate
await performSilentAuth();
break;
case 'unchanged':
// Session is still valid
break;
case 'error':
console.error('Check failed:', result.error);
break;
}
// Cleanup
manager.destroy();Automatically monitors session state with periodic polling.
import { SessionMonitor } from '@authrim/web';
const monitor = new SessionMonitor({
checkSessionIframeUrl: 'https://auth.example.com/connect/checksession',
clientId: 'your-client-id',
opOrigin: 'https://auth.example.com',
pollInterval: 2000, // optional, default: 2000ms
maxErrors: 3, // optional, default: 3
});
// Subscribe to events
const unsubscribe = monitor.on((event) => {
switch (event.type) {
case 'session:changed':
console.log('Session changed! Re-authenticating...');
performSilentAuth().then((newSessionState) => {
monitor.updateSessionState(newSessionState);
});
break;
case 'session:unchanged':
// Session is still valid (optional handling)
break;
case 'session:error':
console.warn('Session check failed');
break;
case 'session:stopped':
console.log('Monitor stopped:', event.reason);
// reason: 'user_stopped' | 'too_many_errors'
break;
}
});
// Start monitoring
await monitor.start(initialSessionState);
// Update session state after re-auth
monitor.updateSessionState(newSessionState);
// Stop monitoring
monitor.stop();
unsubscribe();Handles front-channel logout requests on the RP's logout endpoint.
import { FrontChannelLogoutHandler } from '@authrim/web';
// On your /logout page (loaded in iframe by OP)
const handler = new FrontChannelLogoutHandler({
issuer: 'https://auth.example.com',
sessionId: currentSessionId, // optional, for sid validation
requireIss: true, // require iss parameter
requireSid: false, // require sid parameter
onLogout: async (params) => {
// Clear local session
localStorage.removeItem('session');
sessionStorage.clear();
// Optionally notify your app
window.parent?.postMessage({ type: 'logout' }, '*');
},
});
// Check and handle logout request
if (handler.isLogoutRequest()) {
const result = await handler.handleCurrentUrl();
if (result.success) {
// Show logout confirmation using safe DOM methods
const message = document.createElement('p');
message.textContent = 'You have been logged out.';
document.body.appendChild(message);
} else {
console.error('Logout validation failed:', result.error);
}
}Security Considerations:
- Always enable
requireIss: trueand verify the issuer - Use
requireSid: truewhen session ID is available for CSRF protection - Front-channel logout URI must use HTTPS in production
UI helper for Device Authorization Grant (RFC 8628).
import { DeviceFlowUI, formatUserCode, getDeviceFlowQRCodeUrl } from '@authrim/web';
import { DeviceFlowClient } from '@authrim/core';
// Setup (typically done once)
const httpClient = new BrowserHttpClient();
const deviceClient = new DeviceFlowClient(httpClient, clientId);
const discovery = await fetchDiscovery(issuer);
// Create UI helper
const ui = new DeviceFlowUI({
client: deviceClient,
discovery,
autoPolling: true, // optional, default: true
countdownInterval: 1000, // optional, default: 1000ms
});
// Subscribe to events
const unsubscribe = ui.on((event) => {
switch (event.type) {
case 'device:started':
// Display user code and verification URI
const userCode = formatUserCode(event.state!.userCode); // "ABCD-1234"
const qrUrl = getDeviceFlowQRCodeUrl(event.state!);
showUserCode(userCode);
showQRCode(qrUrl);
showVerificationUri(event.state!.verificationUri);
break;
case 'device:pending':
// Update countdown timer
updateCountdown(event.remainingSeconds!);
break;
case 'device:polling':
// Show polling indicator
showPollingStatus();
break;
case 'device:slow_down':
// OP requested slower polling
console.log('Slowing down polling...');
break;
case 'device:completed':
// Authorization successful!
console.log('Tokens:', event.tokens);
hideDeviceFlowUI();
startApp(event.tokens);
break;
case 'device:expired':
showMessage('Code expired. Please try again.');
break;
case 'device:denied':
showMessage('Authorization was denied.');
break;
case 'device:error':
showError(event.error!.message);
break;
case 'device:cancelled':
showMessage('Authorization cancelled.');
break;
}
});
// Start device flow
await ui.start({ scope: 'openid profile' });
// Cancel if needed (e.g., user clicks cancel button)
cancelButton.onclick = () => ui.cancel();
// Cleanup when done
unsubscribe();Helper Functions:
// Format user code with separator
formatUserCode('ABCD1234'); // "ABCD-1234"
formatUserCode('ABCD1234', ' '); // "ABCD 1234"
formatUserCode('ABCDEF12', '-', 3); // "ABC-DEF-12"
// Get URL for QR code (prefers verification_uri_complete)
const url = getDeviceFlowQRCodeUrl(state);
// Returns verification_uri_complete if available, otherwise verification_uri// Subscribe to auth events
const unsubscribe = auth.on('auth:login', (event) => {
console.log('User logged in:', event.user);
console.log('Method:', event.method); // 'passkey' | 'emailCode' | 'social'
});
auth.on('auth:logout', (event) => {
console.log('User logged out');
if (event.redirectUri) {
window.location.href = event.redirectUri;
}
});
auth.on('token:refreshed', (event) => {
console.log('Token refreshed, session:', event.session);
});
// Unsubscribe
unsubscribe();Available Events:
| Event | Payload | Description |
|---|---|---|
auth:login |
{ session, user, method } |
User logged in |
auth:logout |
{ redirectUri? } |
User logged out |
token:refreshed |
{ session } |
Token was refreshed |
session:changed |
{ session, user } |
Session state changed |
session:expired |
{ reason } |
Session expired |
auth:error |
{ error } |
Authentication error |
All methods return a discriminated union:
type AuthResponse<T> =
| { data: T; error: null }
| { data: null; error: AuthError };
interface AuthError {
code: string; // e.g., 'AR001001'
error: string; // e.g., 'invalid_credentials'
message: string; // Human-readable message
retryable: boolean;
severity: 'info' | 'warn' | 'error' | 'critical';
}Usage:
const { data, error } = await auth.passkey.login();
if (error) {
// Handle error
console.log('Code:', error.code);
console.log('Retryable:', error.retryable);
if (error.retryable) {
showRetryButton();
} else {
showErrorMessage(error.message);
}
return;
}
// data is guaranteed to be non-null here
console.log('User:', data.user);
console.log('Session:', data.session);import { createAuthrim } from '@authrim/web';
// Initialize
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'my-spa',
});
// Event listeners
auth.on('auth:login', ({ user }) => {
updateUI(user);
});
auth.on('auth:logout', () => {
showLoginPage();
});
// Check existing session on load
const { data } = await auth.session.get();
if (data) {
updateUI(data.user);
} else {
showLoginPage();
}
// Login handler
async function handleLogin() {
const { data, error } = await auth.passkey.login();
if (error) {
showError(error.message);
}
}
// Logout handler
async function handleLogout() {
await auth.signOut();
}import { createAuthrim, SessionMonitor } from '@authrim/web';
// Initialize with OAuth
const auth = await createAuthrim({
issuer: 'https://auth.example.com',
clientId: 'my-app',
enableOAuth: true,
});
// Login with popup
const { data, error } = await auth.oauth.popup.login({
scopes: ['openid', 'profile', 'email'],
});
if (error) {
console.error('Login failed:', error.message);
return;
}
// Start session monitoring
const monitor = new SessionMonitor({
checkSessionIframeUrl: 'https://auth.example.com/connect/checksession',
clientId: 'my-app',
opOrigin: 'https://auth.example.com',
});
monitor.on((event) => {
if (event.type === 'session:changed') {
// Attempt silent re-auth
auth.oauth.silentAuth.check({
redirectUri: 'https://app.example.com/silent-callback',
}).then(({ data, error }) => {
if (data) {
monitor.updateSessionState(data.sessionState);
} else {
// Silent auth failed, redirect to login
auth.signOut();
}
});
}
});
await monitor.start(data.sessionState);import { DeviceFlowUI, formatUserCode, getDeviceFlowQRCodeUrl } from '@authrim/web';
// Initialize Device Flow UI
const ui = new DeviceFlowUI({
client: deviceFlowClient,
discovery,
});
// Handle events
ui.on((event) => {
switch (event.type) {
case 'device:started':
console.log('\n=== Authorization Required ===');
console.log(`Visit: ${event.state!.verificationUri}`);
console.log(`Code: ${formatUserCode(event.state!.userCode)}`);
console.log('==============================\n');
break;
case 'device:pending':
process.stdout.write(`\rWaiting... ${event.remainingSeconds}s remaining`);
break;
case 'device:completed':
console.log('\n\nAuthorization successful!');
saveTokens(event.tokens);
break;
case 'device:expired':
console.log('\n\nCode expired. Please restart.');
process.exit(1);
break;
}
});
// Start and wait
await ui.start({ scope: 'openid profile' });| Type | Persistence | XSS Risk | Recommendation |
|---|---|---|---|
memory |
Tab only | Lowest | SPA recommended |
sessionStorage |
Tab/reload | Medium | Default |
localStorage |
Permanent | Highest | Explicit opt-in only |
| Browser | Version | WebAuthn |
|---|---|---|
| Chrome | 67+ | 67+ |
| Firefox | 60+ | 60+ |
| Safari | 13+ | 14+ |
| Edge | 79+ | 79+ |
WebAuthn Requirements:
- HTTPS required (except localhost)
- User gesture required for credential creation
Full TypeScript support with type inference:
import type {
// Main types
Authrim,
AuthrimConfig,
AuthResponse,
AuthError,
AuthSessionData,
User,
Session,
// Namespaces
PasskeyNamespace,
EmailCodeNamespace,
SocialNamespace,
SessionNamespace,
OAuthNamespace,
// Events
AuthEventName,
AuthEventHandler,
AuthEventPayloads,
// Session Management
CheckSessionIframeManagerOptions,
CheckSessionResult,
SessionMonitorOptions,
SessionMonitorEvent,
SessionMonitorEventType,
FrontChannelLogoutHandlerOptions,
FrontChannelLogoutHandleResult,
// Device Flow
DeviceFlowUIOptions,
DeviceFlowUIEvent,
DeviceFlowUIEventType,
} from '@authrim/web';# Install dependencies
pnpm install
# Run tests
pnpm test
# Type check
pnpm typecheck
# Build
pnpm build
# Watch mode
pnpm dev
# Format code
pnpm format
# Lint
pnpm lintApache-2.0
| Package | Description | Status |
|---|---|---|
| @authrim/core | Platform-agnostic core library | ✅ Available |
| @authrim/react | React hooks and components | 🚧 Planned |
| @authrim/sveltekit | SvelteKit integration | ✅ Available |
| @authrim/vue | Vue.js integration | 🚧 Planned |