@svadmin/sso 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,485 +0,0 @@
1
- /**
2
- * @svadmin/sso — OIDC/OAuth2 SSO AuthProvider for svadmin.
3
- *
4
- * Supports any OIDC-compliant Identity Provider:
5
- * - Okta
6
- * - Azure AD / Entra ID
7
- * - Amazon Cognito
8
- * - Keycloak
9
- * - Google Workspace
10
- * - Auth0
11
- * - Any custom OIDC server
12
- *
13
- * Uses the Authorization Code Flow with PKCE (no client secret needed in browser).
14
- *
15
- * @example
16
- * ```ts
17
- * import { createSSOAuthProvider } from '@svadmin/sso';
18
- * import { setAuthProvider } from '@svadmin/core';
19
-
20
-
21
- *
22
- * const authProvider = createSSOAuthProvider({
23
- * issuer: 'https://your-tenant.okta.com',
24
- * clientId: 'your-client-id',
25
- * redirectUri: window.location.origin + '/callback',
26
- * });
27
- *
28
- * setAuthProvider(authProvider);
29
- * ```
30
- */
31
-
32
- import type { AuthProvider, Identity } from '@svadmin/core';
33
-
34
- // Safe base64url decode for JWT payloads
35
- function decodeJwtPayload(token: string): Record<string, unknown> | null {
36
- if (!token) return null;
37
- const parts = token.split('.');
38
- if (parts.length < 2) return null;
39
- const payloadStr = parts[1];
40
- const padded = payloadStr + '='.repeat((4 - (payloadStr.length % 4)) % 4);
41
- const binString = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
42
- const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0) ?? 0);
43
- return JSON.parse(new TextDecoder().decode(bytes));
44
- }
45
-
46
-
47
- // ─── Types ────────────────────────────────────────────────────
48
-
49
- export interface SSOConfig {
50
- /** OIDC Issuer URL (e.g., 'https://your-tenant.okta.com') */
51
- issuer: string;
52
- /** OAuth2 Client ID */
53
- clientId: string;
54
- /** Redirect URI after login (e.g., '/callback') */
55
- redirectUri: string;
56
- /** OAuth2 scopes. Default: ['openid', 'profile', 'email'] */
57
- scopes?: string[];
58
- /** Where to redirect after logout */
59
- postLogoutRedirectUri?: string;
60
- /** Token storage backend. Default: 'local' (localStorage) */
61
- storage?: 'local' | 'session' | TokenStorage;
62
- /** Custom identity mapper — transform OIDC userinfo to svadmin Identity */
63
- mapIdentity?: (userinfo: Record<string, unknown>) => Identity;
64
- /** Auto-refresh tokens before expiry. Default: true */
65
- autoRefresh?: boolean;
66
- /** Seconds before expiry to trigger refresh. Default: 60 */
67
- refreshBuffer?: number;
68
- /** Additional authorization request parameters, e.g. audience or prompt. */
69
- authorizationParams?: Record<string, string>;
70
- /**
71
- * Manual OAuth2 endpoints for providers that don't support OIDC discovery
72
- * (e.g., GitHub). When provided, OIDC auto-discovery is skipped.
73
- */
74
- manualEndpoints?: {
75
- authorization_endpoint: string;
76
- token_endpoint: string;
77
- userinfo_endpoint: string;
78
- end_session_endpoint?: string;
79
- };
80
- }
81
-
82
- export interface TokenStorage {
83
- getItem: (key: string) => string | null;
84
- setItem: (key: string, value: string) => void;
85
- removeItem: (key: string) => void;
86
- }
87
-
88
- export interface SSOAuthProvider extends AuthProvider {
89
- getAccessToken: () => Promise<string | null>;
90
- }
91
-
92
- interface OIDCConfig {
93
- authorization_endpoint: string;
94
- token_endpoint: string;
95
- userinfo_endpoint: string;
96
- end_session_endpoint?: string;
97
- }
98
-
99
- interface TokenSet {
100
- access_token: string;
101
- id_token?: string;
102
- refresh_token?: string;
103
- expires_at?: number;
104
- token_type: string;
105
- }
106
-
107
- // ─── PKCE Utilities ───────────────────────────────────────────
108
-
109
- function generateRandomString(length: number): string {
110
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
111
- const values = crypto.getRandomValues(new Uint8Array(length));
112
- return Array.from(values, (v) => chars[v % chars.length]).join('');
113
- }
114
-
115
- async function sha256(plain: string): Promise<ArrayBuffer> {
116
- const encoder = new TextEncoder();
117
- return crypto.subtle.digest('SHA-256', encoder.encode(plain));
118
- }
119
-
120
- function base64urlEncode(buffer: ArrayBuffer): string {
121
- const bytes = new Uint8Array(buffer);
122
- let binary = '';
123
- for (const byte of bytes) binary += String.fromCharCode(byte);
124
- return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
125
- }
126
-
127
- async function createPKCEChallenge(): Promise<{ verifier: string; challenge: string }> {
128
- const verifier = generateRandomString(64);
129
- const hashed = await sha256(verifier);
130
- const challenge = base64urlEncode(hashed);
131
- return { verifier, challenge };
132
- }
133
-
134
- // ─── Storage Helper ───────────────────────────────────────────
135
-
136
- const STORAGE_PREFIX = 'svadmin_sso_';
137
-
138
- function getStorage(config: SSOConfig): TokenStorage {
139
- if (config.storage && typeof config.storage === 'object') return config.storage;
140
- return {
141
- getItem: (key) => typeof window !== 'undefined' ? (config.storage === 'session' ? sessionStorage : localStorage).getItem(key) : null,
142
- setItem: (key, value) => { if (typeof window !== 'undefined') (config.storage === 'session' ? sessionStorage : localStorage).setItem(key, value); },
143
- removeItem: (key) => { if (typeof window !== 'undefined') (config.storage === 'session' ? sessionStorage : localStorage).removeItem(key); },
144
- };
145
- }
146
-
147
- // ─── Provider ─────────────────────────────────────────────────
148
-
149
- /**
150
- * Create an OIDC/OAuth2 SSO AuthProvider.
151
- */
152
- export function createSSOAuthProvider(config: SSOConfig): SSOAuthProvider {
153
- const scopes = config.scopes ?? ['openid', 'profile', 'email'];
154
- const autoRefresh = config.autoRefresh ?? true;
155
- const refreshBuffer = config.refreshBuffer ?? 60;
156
- const storage = getStorage(config);
157
-
158
- let oidcConfig: OIDCConfig | null = null;
159
- let refreshTimer: ReturnType<typeof setTimeout> | null = null;
160
-
161
- // ── OIDC Discovery ───────────────────────────────────────
162
-
163
- async function discover(): Promise<OIDCConfig> {
164
- if (oidcConfig) return oidcConfig;
165
-
166
- // Use manual endpoints if provided (e.g., GitHub)
167
- if (config.manualEndpoints) {
168
- oidcConfig = config.manualEndpoints;
169
- return oidcConfig;
170
- }
171
-
172
- const url = `${config.issuer.replace(/\/$/, '')}/.well-known/openid-configuration`;
173
- const res = await fetch(url);
174
- if (!res.ok) throw new Error(`OIDC discovery failed: ${res.status} ${res.statusText}`);
175
- oidcConfig = await res.json() as OIDCConfig;
176
- return oidcConfig;
177
- }
178
-
179
- // ── Token Management ─────────────────────────────────────
180
-
181
- function getTokens(): TokenSet | null {
182
- const raw = storage.getItem(`${STORAGE_PREFIX}tokens`);
183
- if (!raw) return null;
184
- try { return JSON.parse(raw) as TokenSet; }
185
- catch { return null; }
186
- }
187
-
188
- function setTokens(tokens: TokenSet): void {
189
- storage.setItem(`${STORAGE_PREFIX}tokens`, JSON.stringify(tokens));
190
- if (autoRefresh && tokens.refresh_token && tokens.expires_at) {
191
- scheduleRefresh(tokens);
192
- }
193
- }
194
-
195
- function clearTokens(): void {
196
- storage.removeItem(`${STORAGE_PREFIX}tokens`);
197
- storage.removeItem(`${STORAGE_PREFIX}pkce_verifier`);
198
- storage.removeItem(`${STORAGE_PREFIX}state`);
199
- if (refreshTimer) clearTimeout(refreshTimer);
200
- }
201
-
202
- function scheduleRefresh(tokens: TokenSet): void {
203
- if (refreshTimer) clearTimeout(refreshTimer);
204
- if (!tokens.expires_at || !tokens.refresh_token) return;
205
- const now = Math.floor(Date.now() / 1000);
206
- const delay = Math.max(0, (tokens.expires_at - now - refreshBuffer)) * 1000;
207
- const refreshToken = tokens.refresh_token;
208
- refreshTimer = setTimeout(() => refreshAccessToken(refreshToken), delay);
209
- }
210
-
211
- async function refreshAccessToken(refreshToken: string): Promise<void> {
212
- try {
213
- const cfg = await discover();
214
- const body = new URLSearchParams({
215
- grant_type: 'refresh_token',
216
- client_id: config.clientId,
217
- refresh_token: refreshToken,
218
- });
219
- const res = await fetch(cfg.token_endpoint, {
220
- method: 'POST',
221
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
222
- body,
223
- });
224
- if (!res.ok) { clearTokens(); return; }
225
- const data = await res.json();
226
- if (!data.access_token) {
227
- clearTokens();
228
- return;
229
- }
230
- const newTokens: TokenSet = {
231
- access_token: data.access_token,
232
- id_token: data.id_token,
233
- refresh_token: data.refresh_token ?? refreshToken,
234
- expires_at: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : undefined,
235
- token_type: data.token_type ?? 'Bearer',
236
- };
237
- setTokens(newTokens);
238
- } catch {
239
- clearTokens();
240
- }
241
- }
242
-
243
- // ── Token Exchange ───────────────────────────────────────
244
-
245
- async function exchangeCode(code: string): Promise<TokenSet> {
246
- const cfg = await discover();
247
- const verifier = storage.getItem(`${STORAGE_PREFIX}pkce_verifier`);
248
- const body = new URLSearchParams({
249
- grant_type: 'authorization_code',
250
- client_id: config.clientId,
251
- code,
252
- redirect_uri: config.redirectUri,
253
- ...(verifier ? { code_verifier: verifier } : {}),
254
- });
255
- const res = await fetch(cfg.token_endpoint, {
256
- method: 'POST',
257
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
258
- body,
259
- });
260
- if (!res.ok) throw new Error(`Token exchange failed: ${res.status}`);
261
- const data = await res.json();
262
- if (!data.access_token) throw new Error('Token exchange failed: missing access_token');
263
- const tokens: TokenSet = {
264
- access_token: data.access_token,
265
- id_token: data.id_token,
266
- refresh_token: data.refresh_token,
267
- expires_at: data.expires_in ? Math.floor(Date.now() / 1000) + data.expires_in : undefined,
268
- token_type: data.token_type ?? 'Bearer',
269
- };
270
- setTokens(tokens);
271
- storage.removeItem(`${STORAGE_PREFIX}pkce_verifier`);
272
- storage.removeItem(`${STORAGE_PREFIX}state`);
273
- return tokens;
274
- }
275
-
276
- // ── Default Identity Mapper ──────────────────────────────
277
-
278
- function defaultMapIdentity(userinfo: Record<string, unknown>): Identity {
279
- return {
280
- id: (userinfo.sub as string) ?? '',
281
- name: (userinfo.name as string) ?? (userinfo.preferred_username as string) ?? '',
282
- avatar: (userinfo.picture as string) ?? undefined,
283
- email: (userinfo.email as string) ?? undefined,
284
- };
285
- }
286
-
287
- const mapIdentity = config.mapIdentity ?? defaultMapIdentity;
288
-
289
- // ── AuthProvider Implementation ──────────────────────────
290
-
291
- return {
292
- async login() {
293
- if (typeof window === 'undefined') {
294
- return { success: false, error: { message: 'SSO login requires a browser environment' } };
295
- }
296
- try {
297
- const cfg = await discover();
298
- const { verifier, challenge } = await createPKCEChallenge();
299
- const state = generateRandomString(32);
300
-
301
- storage.setItem(`${STORAGE_PREFIX}pkce_verifier`, verifier);
302
- storage.setItem(`${STORAGE_PREFIX}state`, state);
303
-
304
- const params = new URLSearchParams({
305
- response_type: 'code',
306
- client_id: config.clientId,
307
- redirect_uri: config.redirectUri,
308
- scope: scopes.join(' '),
309
- state,
310
- code_challenge: challenge,
311
- code_challenge_method: 'S256',
312
- ...config.authorizationParams,
313
- });
314
-
315
- // Redirect to authorization endpoint
316
- window.location.href = `${cfg.authorization_endpoint}?${params}`;
317
-
318
- // Won't actually reach here due to redirect
319
- return { success: true };
320
- } catch (err) {
321
- return { success: false, error: { message: err instanceof Error ? err.message : 'SSO login failed' } };
322
- }
323
- },
324
-
325
- async logout() {
326
- const cfg = await discover();
327
- const tokens = getTokens();
328
- clearTokens();
329
-
330
- if (typeof window !== 'undefined' && cfg.end_session_endpoint && tokens?.id_token) {
331
- const params = new URLSearchParams({
332
- id_token_hint: tokens.id_token,
333
- ...(config.postLogoutRedirectUri
334
- ? { post_logout_redirect_uri: config.postLogoutRedirectUri }
335
- : {}),
336
- });
337
- window.location.href = `${cfg.end_session_endpoint}?${params}`;
338
- return { success: true };
339
- }
340
-
341
- return {
342
- success: true,
343
- redirectTo: config.postLogoutRedirectUri ?? '/',
344
- };
345
- },
346
-
347
- async check() {
348
- // SSR: no tokens, no callback — return unauthenticated
349
- if (typeof window === 'undefined') {
350
- const tokens = getTokens();
351
- if (tokens && (!tokens.expires_at || tokens.expires_at > Math.floor(Date.now() / 1000))) {
352
- return { authenticated: true };
353
- }
354
- return { authenticated: false, logout: true };
355
- }
356
-
357
- // Handle callback — exchange authorization code for tokens
358
- const url = new URL(window.location.href);
359
- const callbackError = url.searchParams.get('error');
360
- if (callbackError) {
361
- clearTokens();
362
- return {
363
- authenticated: false,
364
- error: {
365
- message: url.searchParams.get('error_description') ?? callbackError,
366
- name: callbackError,
367
- },
368
- logout: true,
369
- };
370
- }
371
-
372
- const code = url.searchParams.get('code');
373
- const state = url.searchParams.get('state');
374
-
375
- if (code) {
376
- const savedState = storage.getItem(`${STORAGE_PREFIX}state`);
377
- if (!state || !savedState || state !== savedState) {
378
- clearTokens();
379
- return { authenticated: false, error: { message: 'State mismatch' } };
380
- }
381
- try {
382
- await exchangeCode(code);
383
- // Clean URL
384
- url.searchParams.delete('code');
385
- url.searchParams.delete('state');
386
- window.history.replaceState({}, '', `${url.pathname}${url.search}${url.hash}`);
387
- return { authenticated: true };
388
- } catch (err) {
389
- return { authenticated: false, error: { message: String(err) } };
390
- }
391
- }
392
-
393
- // Check existing tokens
394
- const tokens = getTokens();
395
- if (!tokens) {
396
- return { authenticated: false, logout: true };
397
- }
398
-
399
- // Check expiry
400
- if (tokens.expires_at && tokens.expires_at < Math.floor(Date.now() / 1000)) {
401
- if (tokens.refresh_token) {
402
- await refreshAccessToken(tokens.refresh_token);
403
- const refreshed = getTokens();
404
- if (refreshed) return { authenticated: true };
405
- }
406
- clearTokens();
407
- return { authenticated: false, logout: true };
408
- }
409
-
410
- // Schedule refresh if not already done
411
- if (autoRefresh && tokens.refresh_token && tokens.expires_at) {
412
- scheduleRefresh(tokens);
413
- }
414
-
415
- return { authenticated: true };
416
- },
417
-
418
- async getIdentity(): Promise<Identity | null> {
419
- const tokens = getTokens();
420
- if (!tokens) return null;
421
-
422
- try {
423
- const cfg = await discover();
424
- const res = await fetch(cfg.userinfo_endpoint, {
425
- headers: { Authorization: `${tokens.token_type} ${tokens.access_token}` },
426
- });
427
- if (!res.ok) return null;
428
- const userinfo = await res.json();
429
- return mapIdentity(userinfo);
430
- } catch {
431
- return null;
432
- }
433
- },
434
-
435
- async getPermissions() {
436
- const tokens = getTokens();
437
- if (!tokens) return null;
438
-
439
- // Try to extract roles/permissions from the ID token
440
- if (tokens.id_token) {
441
- try {
442
- const payload = decodeJwtPayload(tokens.id_token);
443
- if (!payload) return null;
444
- return payload.roles ?? payload.groups ?? payload.permissions ?? null;
445
- } catch {
446
- return null;
447
- }
448
- }
449
-
450
- return null;
451
- },
452
-
453
- async getAccessToken() {
454
- const tokens = getTokens();
455
- if (!tokens) return null;
456
-
457
- if (tokens.expires_at && tokens.expires_at < Math.floor(Date.now() / 1000)) {
458
- if (!tokens.refresh_token) {
459
- clearTokens();
460
- return null;
461
- }
462
- await refreshAccessToken(tokens.refresh_token);
463
- return getTokens()?.access_token ?? null;
464
- }
465
-
466
- return tokens.access_token;
467
- },
468
-
469
- async onError(error) {
470
- if (typeof error === 'object' && error !== null && 'statusCode' in error) {
471
- const statusCode = (error as { statusCode: number }).statusCode;
472
- if (statusCode === 401 || statusCode === 403) {
473
- return { logout: true, redirectTo: '/login' };
474
- }
475
- }
476
- if (typeof error === 'object' && error !== null && 'status' in error) {
477
- const status = (error as { status: number }).status;
478
- if (status === 401 || status === 403) {
479
- return { logout: true, redirectTo: '/login' };
480
- }
481
- }
482
- return {};
483
- },
484
- };
485
- }
package/src/index.ts DELETED
@@ -1,22 +0,0 @@
1
- /**
2
- * @svadmin/sso — OIDC/OAuth2 SSO AuthProvider plugin
3
- *
4
- * Usage:
5
- * ```ts
6
- * import { createSSOAuthProvider, createGoogleAuth } from '@svadmin/sso';
7
- * ```
8
- */
9
-
10
- export { createSSOAuthProvider } from './auth-provider';
11
- export type { SSOAuthProvider, SSOConfig, TokenStorage } from './auth-provider';
12
-
13
- // Social login presets
14
- export {
15
- createGoogleAuth,
16
- createMicrosoftAuth,
17
- createGitHubAuth,
18
- createGitLabAuth,
19
- createKeycloakAuth,
20
- createAuth0Auth,
21
- } from './presets';
22
- export type { PresetOptions } from './presets';
package/src/presets.ts DELETED
@@ -1,179 +0,0 @@
1
- /**
2
- * Social login presets — pre-configured SSO providers for common identity platforms.
3
- *
4
- * Each factory function returns an AuthProvider via `createSSOAuthProvider()`
5
- * with the correct issuer and default scopes for that platform.
6
- *
7
- * @example
8
- * ```ts
9
- * import { createGoogleAuth } from '@svadmin/sso';
10
- * import { setAuthProvider } from '@svadmin/core';
11
- *
12
- * setAuthProvider(createGoogleAuth('your-client-id'));
13
- * ```
14
- */
15
-
16
- import { createSSOAuthProvider, type SSOConfig } from './auth-provider';
17
-
18
- // ─── Shared helpers ──────────────────────────────────────────
19
-
20
- type PresetOptions = Omit<SSOConfig, 'issuer' | 'clientId'>;
21
-
22
- function resolveRedirectUri(opts?: PresetOptions): string {
23
- return opts?.redirectUri ?? (typeof window !== 'undefined' ? `${window.location.origin}/callback` : '/callback');
24
- }
25
-
26
- // ─── Google ──────────────────────────────────────────────────
27
-
28
- /**
29
- * Google Sign-In via OIDC (OpenID Connect).
30
- *
31
- * Prerequisites:
32
- * 1. Create OAuth2 credentials at https://console.cloud.google.com/apis/credentials
33
- * 2. Add authorized redirect URIs (e.g., `http://localhost:5173/callback`)
34
- *
35
- * @param clientId - Google OAuth2 Client ID
36
- */
37
- export function createGoogleAuth(clientId: string, opts?: PresetOptions) {
38
- return createSSOAuthProvider({
39
- issuer: 'https://accounts.google.com',
40
- clientId,
41
- scopes: ['openid', 'profile', 'email'],
42
- redirectUri: resolveRedirectUri(opts),
43
- ...opts,
44
- });
45
- }
46
-
47
- // ─── Microsoft / Azure AD (Entra ID) ────────────────────────
48
-
49
- /**
50
- * Microsoft / Azure AD Sign-In via OIDC.
51
- *
52
- * @param clientId - Azure App Registration Client ID
53
- * @param tenantId - Azure tenant ID. Default: 'common' (multi-tenant)
54
- */
55
- export function createMicrosoftAuth(
56
- clientId: string,
57
- tenantId = 'common',
58
- opts?: PresetOptions,
59
- ) {
60
- return createSSOAuthProvider({
61
- issuer: `https://login.microsoftonline.com/${tenantId}/v2.0`,
62
- clientId,
63
- scopes: ['openid', 'profile', 'email'],
64
- redirectUri: resolveRedirectUri(opts),
65
- ...opts,
66
- });
67
- }
68
-
69
- // ─── GitHub ──────────────────────────────────────────────────
70
-
71
- /**
72
- * GitHub Sign-In via OAuth2.
73
- *
74
- * Note: GitHub does NOT support OIDC discovery, so we manually specify endpoints.
75
- * The `issuer` field is used for identification only.
76
- *
77
- * Prerequisites:
78
- * 1. Create an OAuth App at https://github.com/settings/developers
79
- * 2. Set the callback URL to your redirect URI
80
- *
81
- * @param clientId - GitHub OAuth App Client ID
82
- */
83
- export function createGitHubAuth(clientId: string, opts?: PresetOptions) {
84
- // GitHub uses standard OAuth2, not OIDC — requires manual endpoint config.
85
- // We leverage the SSO provider but override the discovery with manual endpoints.
86
- const redirectUri = resolveRedirectUri(opts);
87
-
88
- return createSSOAuthProvider({
89
- issuer: 'https://github.com',
90
- clientId,
91
- scopes: ['read:user', 'user:email'],
92
- redirectUri,
93
- // GitHub doesn't support OIDC discovery, provide manual endpoints
94
- manualEndpoints: {
95
- authorization_endpoint: 'https://github.com/login/oauth/authorize',
96
- token_endpoint: 'https://github.com/login/oauth/access_token',
97
- userinfo_endpoint: 'https://api.github.com/user',
98
- },
99
- mapIdentity: (userinfo) => ({
100
- id: String(userinfo.id ?? ''),
101
- name: (userinfo.name as string) ?? (userinfo.login as string) ?? '',
102
- avatar: (userinfo.avatar_url as string) ?? undefined,
103
- email: (userinfo.email as string) ?? undefined,
104
- }),
105
- ...opts,
106
- });
107
- }
108
-
109
- // ─── GitLab ──────────────────────────────────────────────────
110
-
111
- /**
112
- * GitLab Sign-In via OIDC.
113
- *
114
- * @param clientId - GitLab Application ID
115
- * @param instanceUrl - GitLab instance URL. Default: 'https://gitlab.com'
116
- */
117
- export function createGitLabAuth(
118
- clientId: string,
119
- instanceUrl = 'https://gitlab.com',
120
- opts?: PresetOptions,
121
- ) {
122
- return createSSOAuthProvider({
123
- issuer: instanceUrl,
124
- clientId,
125
- scopes: ['openid', 'profile', 'email'],
126
- redirectUri: resolveRedirectUri(opts),
127
- ...opts,
128
- });
129
- }
130
-
131
- // ─── Keycloak ────────────────────────────────────────────────
132
-
133
- /**
134
- * Keycloak Sign-In via OIDC.
135
- *
136
- * @param baseUrl - Keycloak base URL (e.g., 'https://keycloak.example.com')
137
- * @param realm - Keycloak realm name
138
- * @param clientId - Keycloak Client ID
139
- */
140
- export function createKeycloakAuth(
141
- baseUrl: string,
142
- realm: string,
143
- clientId: string,
144
- opts?: PresetOptions,
145
- ) {
146
- return createSSOAuthProvider({
147
- issuer: `${baseUrl.replace(/\/$/, '')}/realms/${realm}`,
148
- clientId,
149
- scopes: ['openid', 'profile', 'email'],
150
- redirectUri: resolveRedirectUri(opts),
151
- ...opts,
152
- });
153
- }
154
-
155
- // ─── Auth0 ───────────────────────────────────────────────────
156
-
157
- /**
158
- * Auth0 Sign-In via OIDC.
159
- *
160
- * @param domain - Auth0 domain (e.g., 'your-tenant.auth0.com')
161
- * @param clientId - Auth0 Client ID
162
- */
163
- export function createAuth0Auth(
164
- domain: string,
165
- clientId: string,
166
- opts?: PresetOptions,
167
- ) {
168
- return createSSOAuthProvider({
169
- issuer: `https://${domain.replace(/^https?:\/\//, '').replace(/\/$/, '')}`,
170
- clientId,
171
- scopes: ['openid', 'profile', 'email'],
172
- redirectUri: resolveRedirectUri(opts),
173
- ...opts,
174
- });
175
- }
176
-
177
- // ─── Type exports ────────────────────────────────────────────
178
-
179
- export type { PresetOptions };