@svadmin/sso 0.11.21 → 0.12.7
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.
- package/dist/auth-helpers.d.ts +11 -11
- package/dist/auth-helpers.js +14 -14
- package/dist/auth-provider.js +63 -33
- package/dist/authenticated-fetch.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +13 -11
- package/dist/presets.d.ts +4 -6
- package/dist/presets.js +10 -31
- package/dist/session-manager.js +7 -7
- package/package.json +3 -3
package/dist/auth-helpers.d.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Browser-side OIDC/OAuth2 authentication helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides PKCE generation, OIDC Discovery endpoint resolution, redirect validation,
|
|
5
|
+
* and reverse-proxy origin recovery.
|
|
6
6
|
*/
|
|
7
|
-
/**
|
|
7
|
+
/** Generates a random OAuth state value. */
|
|
8
8
|
export declare function generateState(): string;
|
|
9
|
-
/**
|
|
9
|
+
/** Generates a PKCE code verifier. */
|
|
10
10
|
export declare function generateVerifier(): string;
|
|
11
|
-
/**
|
|
11
|
+
/** Computes the S256 code challenge from a code verifier. */
|
|
12
12
|
export declare function generateChallenge(verifier: string): Promise<string>;
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Validates a returnTo redirect and prevents open-redirect vulnerabilities.
|
|
15
|
+
* Allows only same-origin relative paths (starting with / but not //).
|
|
16
16
|
*/
|
|
17
17
|
export declare function isValidReturnTo(url: string): boolean;
|
|
18
|
-
/**
|
|
18
|
+
/** Recovers the public origin from reverse-proxy headers. */
|
|
19
19
|
export declare function getForwardedOrigin(req: Request, fallbackUrl: URL): string;
|
|
20
|
-
/**
|
|
20
|
+
/** Resolves the authorization endpoint URL through OIDC Discovery. */
|
|
21
21
|
export declare function resolveAuthorizeUrl(issuer: string, overrideUrl?: string, fetcher?: typeof fetch): Promise<string>;
|
|
22
|
-
/**
|
|
22
|
+
/** Resolves the token endpoint URL through OIDC Discovery. */
|
|
23
23
|
export declare function resolveTokenUrl(issuer: string, overrideUrlOrFetcher?: string | typeof fetch, fetcherOverride?: typeof fetch): Promise<string>;
|
package/dist/auth-helpers.js
CHANGED
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Browser-side OIDC/OAuth2 authentication helpers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Provides PKCE generation, OIDC Discovery endpoint resolution, redirect validation,
|
|
5
|
+
* and reverse-proxy origin recovery.
|
|
6
6
|
*/
|
|
7
|
-
/**
|
|
7
|
+
/** Encodes a Uint8Array as base64url using only Web APIs. */
|
|
8
8
|
function base64url(array) {
|
|
9
9
|
let binary = "";
|
|
10
10
|
for (const byte of array)
|
|
11
11
|
binary += String.fromCharCode(byte);
|
|
12
12
|
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
13
13
|
}
|
|
14
|
-
/**
|
|
14
|
+
/** Generates a random OAuth state value. */
|
|
15
15
|
export function generateState() {
|
|
16
16
|
const array = new Uint8Array(16);
|
|
17
17
|
crypto.getRandomValues(array);
|
|
18
18
|
return base64url(array);
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/** Generates a PKCE code verifier. */
|
|
21
21
|
export function generateVerifier() {
|
|
22
22
|
const array = new Uint8Array(32);
|
|
23
23
|
crypto.getRandomValues(array);
|
|
24
24
|
return base64url(array);
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/** Computes the S256 code challenge from a code verifier. */
|
|
27
27
|
export async function generateChallenge(verifier) {
|
|
28
28
|
const encoder = new TextEncoder();
|
|
29
29
|
const data = encoder.encode(verifier);
|
|
@@ -31,8 +31,8 @@ export async function generateChallenge(verifier) {
|
|
|
31
31
|
return base64url(new Uint8Array(hash));
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
34
|
+
* Validates a returnTo redirect and prevents open-redirect vulnerabilities.
|
|
35
|
+
* Allows only same-origin relative paths (starting with / but not //).
|
|
36
36
|
*/
|
|
37
37
|
export function isValidReturnTo(url) {
|
|
38
38
|
if (!url)
|
|
@@ -41,7 +41,7 @@ export function isValidReturnTo(url) {
|
|
|
41
41
|
return false;
|
|
42
42
|
return /^\/[^/].*/.test(url) || url === "/";
|
|
43
43
|
}
|
|
44
|
-
/**
|
|
44
|
+
/** Recovers the public origin from reverse-proxy headers. */
|
|
45
45
|
export function getForwardedOrigin(req, fallbackUrl) {
|
|
46
46
|
const forwardedProto = req.headers.get("x-forwarded-proto")?.split(",")[0]?.trim();
|
|
47
47
|
const forwardedHost = req.headers.get("x-forwarded-host")?.split(",")[0]?.trim();
|
|
@@ -49,7 +49,7 @@ export function getForwardedOrigin(req, fallbackUrl) {
|
|
|
49
49
|
const protocol = (forwardedProto || fallbackUrl.protocol.replace(/:$/, "")).replace(/:$/, "");
|
|
50
50
|
return `${protocol}://${host}`;
|
|
51
51
|
}
|
|
52
|
-
/**
|
|
52
|
+
/** Resolves the authorization endpoint URL through OIDC Discovery. */
|
|
53
53
|
export async function resolveAuthorizeUrl(issuer, overrideUrl = "", fetcher = fetch) {
|
|
54
54
|
const normalizedIssuer = issuer.replace(/\/$/, "");
|
|
55
55
|
const normalizedOverride = overrideUrl.trim();
|
|
@@ -65,14 +65,14 @@ export async function resolveAuthorizeUrl(issuer, overrideUrl = "", fetcher = fe
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
catch {
|
|
68
|
-
//
|
|
68
|
+
// Ignore discovery failures and use the fallback.
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
if (normalizedIssuer)
|
|
72
72
|
return `${normalizedIssuer}/oauth/authorize`;
|
|
73
73
|
return "";
|
|
74
74
|
}
|
|
75
|
-
/**
|
|
75
|
+
/** Resolves the token endpoint URL through OIDC Discovery. */
|
|
76
76
|
export async function resolveTokenUrl(issuer, overrideUrlOrFetcher = "", fetcherOverride = fetch) {
|
|
77
77
|
const normalizedIssuer = issuer.replace(/\/$/, "");
|
|
78
78
|
const fetcher = typeof overrideUrlOrFetcher === "function" ? overrideUrlOrFetcher : fetcherOverride;
|
|
@@ -91,7 +91,7 @@ export async function resolveTokenUrl(issuer, overrideUrlOrFetcher = "", fetcher
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
catch {
|
|
94
|
-
//
|
|
94
|
+
// Ignore discovery failures.
|
|
95
95
|
}
|
|
96
96
|
return `${normalizedIssuer}/oauth/token`;
|
|
97
97
|
}
|
package/dist/auth-provider.js
CHANGED
|
@@ -14,26 +14,30 @@ const REQUIRED_OIDC_ENDPOINTS = [
|
|
|
14
14
|
'userinfo_endpoint',
|
|
15
15
|
];
|
|
16
16
|
function readOIDCConfig(body) {
|
|
17
|
-
const discovery =
|
|
18
|
-
? body
|
|
19
|
-
: {};
|
|
17
|
+
const discovery = isUnknownRecord(body) ? body : {};
|
|
20
18
|
const missingEndpoints = REQUIRED_OIDC_ENDPOINTS.filter((endpoint) => {
|
|
21
19
|
const value = discovery[endpoint];
|
|
22
20
|
return typeof value !== 'string' || value.length === 0;
|
|
23
21
|
});
|
|
24
|
-
|
|
22
|
+
const authorizationEndpoint = discovery['authorization_endpoint'];
|
|
23
|
+
const tokenEndpoint = discovery['token_endpoint'];
|
|
24
|
+
const userinfoEndpoint = discovery['userinfo_endpoint'];
|
|
25
|
+
if (missingEndpoints.length > 0 ||
|
|
26
|
+
typeof authorizationEndpoint !== 'string' ||
|
|
27
|
+
typeof tokenEndpoint !== 'string' ||
|
|
28
|
+
typeof userinfoEndpoint !== 'string') {
|
|
25
29
|
throw new SSOAuthError(`OIDC discovery returned incomplete endpoints: missing ${missingEndpoints.join(', ')}`, 502, {
|
|
26
30
|
code: 'invalid_discovery_document',
|
|
27
31
|
body,
|
|
28
32
|
});
|
|
29
33
|
}
|
|
30
34
|
return {
|
|
31
|
-
authorization_endpoint:
|
|
32
|
-
token_endpoint:
|
|
33
|
-
userinfo_endpoint:
|
|
34
|
-
...(typeof discovery
|
|
35
|
-
&& discovery
|
|
36
|
-
? { end_session_endpoint: discovery
|
|
35
|
+
authorization_endpoint: authorizationEndpoint,
|
|
36
|
+
token_endpoint: tokenEndpoint,
|
|
37
|
+
userinfo_endpoint: userinfoEndpoint,
|
|
38
|
+
...(typeof discovery['end_session_endpoint'] === 'string'
|
|
39
|
+
&& discovery['end_session_endpoint'].length > 0
|
|
40
|
+
? { end_session_endpoint: discovery['end_session_endpoint'] }
|
|
37
41
|
: {}),
|
|
38
42
|
};
|
|
39
43
|
}
|
|
@@ -44,18 +48,24 @@ function decodeJwtPayload(token) {
|
|
|
44
48
|
if (parts.length < 2)
|
|
45
49
|
return null;
|
|
46
50
|
const payloadStr = parts[1];
|
|
51
|
+
if (!payloadStr)
|
|
52
|
+
return null;
|
|
47
53
|
const padded = payloadStr + '='.repeat((4 - (payloadStr.length % 4)) % 4);
|
|
48
54
|
const binString = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
|
|
49
55
|
const bytes = Uint8Array.from(binString, (value) => value.codePointAt(0) ?? 0);
|
|
50
|
-
|
|
56
|
+
const payload = JSON.parse(new TextDecoder().decode(bytes));
|
|
57
|
+
return isUnknownRecord(payload) ? payload : null;
|
|
58
|
+
}
|
|
59
|
+
function isUnknownRecord(value) {
|
|
60
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
51
61
|
}
|
|
52
62
|
function getAccessTokenExpiry(data, accessToken) {
|
|
53
|
-
if (typeof data
|
|
54
|
-
return Math.floor(Date.now() / 1000) + data
|
|
63
|
+
if (typeof data['expires_in'] === 'number' && Number.isFinite(data['expires_in'])) {
|
|
64
|
+
return Math.floor(Date.now() / 1000) + data['expires_in'];
|
|
55
65
|
}
|
|
56
66
|
try {
|
|
57
67
|
const payload = decodeJwtPayload(accessToken);
|
|
58
|
-
return typeof payload?.exp === 'number' ? payload
|
|
68
|
+
return typeof payload?.['exp'] === 'number' ? payload['exp'] : undefined;
|
|
59
69
|
}
|
|
60
70
|
catch {
|
|
61
71
|
return undefined;
|
|
@@ -69,7 +79,7 @@ function createInvalidTokenResponseError(fallbackMessage, field, body) {
|
|
|
69
79
|
});
|
|
70
80
|
}
|
|
71
81
|
function readTokenResponse(body, fallbackMessage) {
|
|
72
|
-
if (
|
|
82
|
+
if (isUnknownRecord(body)) {
|
|
73
83
|
return body;
|
|
74
84
|
}
|
|
75
85
|
throw createInvalidTokenResponseError(fallbackMessage, 'response body', body);
|
|
@@ -91,11 +101,14 @@ function authorizationScheme(tokenType) {
|
|
|
91
101
|
function normalizeTokenResponse(body, defaults, fallbackMessage) {
|
|
92
102
|
const response = readTokenResponse(body, fallbackMessage);
|
|
93
103
|
const accessToken = requireTokenString(response, 'access_token', fallbackMessage);
|
|
104
|
+
const idToken = typeof response['id_token'] === 'string' ? response['id_token'] : defaults.idToken;
|
|
105
|
+
const refreshToken = readOptionalTokenString(response, 'refresh_token', defaults.refreshToken, fallbackMessage);
|
|
106
|
+
const expiresAt = getAccessTokenExpiry(response, accessToken);
|
|
94
107
|
return {
|
|
95
108
|
access_token: accessToken,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
expires_at:
|
|
109
|
+
...(idToken === undefined ? {} : { id_token: idToken }),
|
|
110
|
+
...(refreshToken === undefined ? {} : { refresh_token: refreshToken }),
|
|
111
|
+
...(expiresAt === undefined ? {} : { expires_at: expiresAt }),
|
|
99
112
|
token_type: authorizationScheme(readOptionalTokenString(response, 'token_type', defaults.tokenType, fallbackMessage) ?? defaults.tokenType),
|
|
100
113
|
};
|
|
101
114
|
}
|
|
@@ -261,7 +274,8 @@ export function createSSOAuthProvider(config) {
|
|
|
261
274
|
if (!response.ok)
|
|
262
275
|
throw await createSSOAuthResponseError(response, fallbackMessage);
|
|
263
276
|
try {
|
|
264
|
-
|
|
277
|
+
const body = await response.json();
|
|
278
|
+
return body;
|
|
265
279
|
}
|
|
266
280
|
catch (error) {
|
|
267
281
|
throw new SSOAuthError(`${fallbackMessage}: invalid JSON response`, 502, {
|
|
@@ -284,7 +298,7 @@ export function createSSOAuthProvider(config) {
|
|
|
284
298
|
refresh_token: current.refresh_token,
|
|
285
299
|
}), 'Token refresh failed');
|
|
286
300
|
return normalizeTokenResponse(response, {
|
|
287
|
-
idToken: current.id_token,
|
|
301
|
+
...(current.id_token === undefined ? {} : { idToken: current.id_token }),
|
|
288
302
|
refreshToken: current.refresh_token,
|
|
289
303
|
tokenType: current.token_type,
|
|
290
304
|
}, 'Token refresh failed');
|
|
@@ -294,9 +308,9 @@ export function createSSOAuthProvider(config) {
|
|
|
294
308
|
storageKey,
|
|
295
309
|
autoRefresh: config.autoRefresh ?? true,
|
|
296
310
|
refreshBuffer,
|
|
297
|
-
refreshLock: config.refreshLock,
|
|
311
|
+
...(config.refreshLock === undefined ? {} : { refreshLock: config.refreshLock }),
|
|
298
312
|
refresh: performRefresh,
|
|
299
|
-
legacyStorageKey: config.legacyStorageKey,
|
|
313
|
+
...(config.legacyStorageKey === undefined ? {} : { legacyStorageKey: config.legacyStorageKey }),
|
|
300
314
|
});
|
|
301
315
|
function assertAuthorizationExchangeCurrent(authGeneration, expectedState) {
|
|
302
316
|
if (sessions.getAuthGeneration() === authGeneration
|
|
@@ -341,12 +355,26 @@ export function createSSOAuthProvider(config) {
|
|
|
341
355
|
return session;
|
|
342
356
|
});
|
|
343
357
|
}
|
|
344
|
-
const mapIdentity = config.mapIdentity ?? ((userinfo) =>
|
|
345
|
-
id
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
358
|
+
const mapIdentity = config.mapIdentity ?? ((userinfo) => {
|
|
359
|
+
const id = userinfo['sub'];
|
|
360
|
+
if (typeof id !== 'string' || !id)
|
|
361
|
+
throw new Error('Invalid userinfo subject');
|
|
362
|
+
const name = userinfo['name'];
|
|
363
|
+
const username = userinfo['preferred_username'];
|
|
364
|
+
const avatar = userinfo['picture'];
|
|
365
|
+
const email = userinfo['email'];
|
|
366
|
+
for (const value of [name, username, avatar, email]) {
|
|
367
|
+
if (value !== undefined && typeof value !== 'string') {
|
|
368
|
+
throw new Error('Invalid userinfo profile field');
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
id,
|
|
373
|
+
...(typeof name === 'string' ? { name } : typeof username === 'string' ? { name: username } : {}),
|
|
374
|
+
...(typeof avatar === 'string' ? { avatar } : {}),
|
|
375
|
+
...(typeof email === 'string' ? { email } : {}),
|
|
376
|
+
};
|
|
377
|
+
});
|
|
350
378
|
const authorizationSource = {
|
|
351
379
|
getAuthorizationHeader: async (options) => {
|
|
352
380
|
const accessToken = await sessions.getAccessToken(options);
|
|
@@ -399,7 +427,7 @@ export function createSSOAuthProvider(config) {
|
|
|
399
427
|
await clearOwnedLoginState(state);
|
|
400
428
|
}
|
|
401
429
|
catch {
|
|
402
|
-
//
|
|
430
|
+
// Preserve the original authentication error; cleanup failures must not hide the root cause.
|
|
403
431
|
}
|
|
404
432
|
return { success: false, error: getErrorResult(error) };
|
|
405
433
|
}
|
|
@@ -415,7 +443,7 @@ export function createSSOAuthProvider(config) {
|
|
|
415
443
|
catch (error) {
|
|
416
444
|
if (!(error instanceof SSOAuthError) || error.code !== 'auth_lock_failed')
|
|
417
445
|
throw error;
|
|
418
|
-
//
|
|
446
|
+
// Callback commit also fails without a cross-context lock; local sign-out must still clear the session.
|
|
419
447
|
sessions.clearSession();
|
|
420
448
|
}
|
|
421
449
|
try {
|
|
@@ -435,7 +463,7 @@ export function createSSOAuthProvider(config) {
|
|
|
435
463
|
}
|
|
436
464
|
}
|
|
437
465
|
catch {
|
|
438
|
-
//
|
|
466
|
+
// The local session is cleared; discovery or redirect failures must not restore an authenticated state.
|
|
439
467
|
}
|
|
440
468
|
return {
|
|
441
469
|
success: true,
|
|
@@ -469,7 +497,7 @@ export function createSSOAuthProvider(config) {
|
|
|
469
497
|
await clearOwnedLoginState(savedState);
|
|
470
498
|
}
|
|
471
499
|
catch {
|
|
472
|
-
//
|
|
500
|
+
// Error callbacks still return the IdP's original error; cleanup failures must not hide it.
|
|
473
501
|
}
|
|
474
502
|
}
|
|
475
503
|
return {
|
|
@@ -499,7 +527,7 @@ export function createSSOAuthProvider(config) {
|
|
|
499
527
|
await clearOwnedLoginState(savedState);
|
|
500
528
|
}
|
|
501
529
|
catch {
|
|
502
|
-
//
|
|
530
|
+
// Preserve the original token exchange error.
|
|
503
531
|
}
|
|
504
532
|
return { authenticated: false, error: getErrorResult(error) };
|
|
505
533
|
}
|
|
@@ -546,6 +574,8 @@ export function createSSOAuthProvider(config) {
|
|
|
546
574
|
if (!response.ok)
|
|
547
575
|
return null;
|
|
548
576
|
const userinfo = await response.json();
|
|
577
|
+
if (!isUnknownRecord(userinfo))
|
|
578
|
+
return null;
|
|
549
579
|
if (sessions.getAuthGeneration() !== authGeneration
|
|
550
580
|
|| !sessions.getSession())
|
|
551
581
|
return null;
|
|
@@ -46,7 +46,7 @@ export function createAuthenticatedFetch(source, fetcher, options = {}) {
|
|
|
46
46
|
cause: error,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
//
|
|
49
|
+
// Create both copies before the first send so a streamed body is not silently lost after consumption.
|
|
50
50
|
const firstAttempt = cloneReplayableRequest(request);
|
|
51
51
|
const retryAttempt = cloneReplayableRequest(request);
|
|
52
52
|
const authorization = await source.getAuthorizationHeader();
|
package/dist/errors.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface SSOAuthErrorOptions {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class SSOAuthError extends Error {
|
|
9
9
|
readonly statusCode: number;
|
|
10
|
-
readonly code
|
|
10
|
+
readonly code: string | undefined;
|
|
11
11
|
readonly details?: unknown;
|
|
12
12
|
readonly body?: unknown;
|
|
13
13
|
readonly retryable: boolean;
|
package/dist/errors.js
CHANGED
|
@@ -31,9 +31,10 @@ export class SSOAuthError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
function asRecord(value) {
|
|
34
|
-
return
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
return isRecord(value) ? value : null;
|
|
35
|
+
}
|
|
36
|
+
function isRecord(value) {
|
|
37
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
38
|
}
|
|
38
39
|
function firstString(...values) {
|
|
39
40
|
return values.find((value) => typeof value === 'string');
|
|
@@ -43,7 +44,8 @@ async function readResponseBody(response) {
|
|
|
43
44
|
if (!text)
|
|
44
45
|
return undefined;
|
|
45
46
|
try {
|
|
46
|
-
|
|
47
|
+
const value = JSON.parse(text);
|
|
48
|
+
return value;
|
|
47
49
|
}
|
|
48
50
|
catch {
|
|
49
51
|
return text;
|
|
@@ -52,15 +54,15 @@ async function readResponseBody(response) {
|
|
|
52
54
|
export async function createSSOAuthResponseError(response, fallbackMessage) {
|
|
53
55
|
const body = await readResponseBody(response);
|
|
54
56
|
const record = asRecord(body);
|
|
55
|
-
const nestedError = asRecord(record?.error);
|
|
56
|
-
const code = firstString(record?.code, record?.error_code, nestedError?.code, nestedError?.error_code, record?.error);
|
|
57
|
-
const description = firstString(record?.error_description, nestedError?.error_description);
|
|
58
|
-
const responseMessage = firstString(record?.message, nestedError?.message);
|
|
57
|
+
const nestedError = asRecord(record?.['error']);
|
|
58
|
+
const code = firstString(record?.['code'], record?.['error_code'], nestedError?.['code'], nestedError?.['error_code'], record?.['error']);
|
|
59
|
+
const description = firstString(record?.['error_description'], nestedError?.['error_description']);
|
|
60
|
+
const responseMessage = firstString(record?.['message'], nestedError?.['message']);
|
|
59
61
|
const textMessage = typeof body === 'string' ? body : undefined;
|
|
60
62
|
const statusMessage = response.statusText || `HTTP ${response.status}`;
|
|
61
63
|
return new SSOAuthError(description ?? responseMessage ?? textMessage ?? `${fallbackMessage}: ${statusMessage}`, response.status, {
|
|
62
|
-
code,
|
|
63
|
-
details: record?.details ?? nestedError?.details,
|
|
64
|
+
...(code === undefined ? {} : { code }),
|
|
65
|
+
details: record?.['details'] ?? nestedError?.['details'],
|
|
64
66
|
body,
|
|
65
67
|
});
|
|
66
68
|
}
|
|
@@ -81,6 +83,6 @@ export function createSSOAuthNetworkError(error, fallbackMessage) {
|
|
|
81
83
|
}
|
|
82
84
|
export function isTerminalSessionError(error) {
|
|
83
85
|
const record = asRecord(error);
|
|
84
|
-
const code = firstString(record?.code, record?.error_code, asRecord(record?.error)?.code, asRecord(record?.error)?.error_code);
|
|
86
|
+
const code = firstString(record?.['code'], record?.['error_code'], asRecord(record?.['error'])?.['code'], asRecord(record?.['error'])?.['error_code']);
|
|
85
87
|
return code !== undefined && TERMINAL_OAUTH_CODES.has(code);
|
|
86
88
|
}
|
package/dist/presets.d.ts
CHANGED
|
@@ -68,15 +68,13 @@ export declare function createKeycloakAuth(baseUrl: string, realm: string, clien
|
|
|
68
68
|
export declare function createAuth0Auth(domain: string, clientId: string, opts?: PresetOptions): import("./auth-provider.js").SSOAuthProvider;
|
|
69
69
|
export type { PresetOptions };
|
|
70
70
|
/**
|
|
71
|
-
* Supauth
|
|
71
|
+
* Supauth OIDC Sign-In (GoTrue-compatible identity provider).
|
|
72
72
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `/logout`. Other GoTrue deployments should provide explicit endpoints or use
|
|
76
|
-
* `createSSOAuthProvider()` directly.
|
|
73
|
+
* Compatible with SupaCloud, SupaOAuth, and any GoTrue-based auth backend.
|
|
74
|
+
* Uses standard OIDC Authorization Code Flow with PKCE.
|
|
77
75
|
*
|
|
76
|
+
* @param issuer - Supauth issuer URL (e.g., 'https://auth.example.com')
|
|
78
77
|
* @param clientId - OAuth2 Client ID
|
|
79
|
-
* @param opts - SSO options. `opts.issuer` accepts a Supauth public root or `/auth/v1` issuer URL.
|
|
80
78
|
*/
|
|
81
79
|
export declare function createSupauthAuth(clientId: string, opts?: PresetOptions & {
|
|
82
80
|
issuer?: string;
|
package/dist/presets.js
CHANGED
|
@@ -83,10 +83,10 @@ export function createGitHubAuth(clientId, opts) {
|
|
|
83
83
|
userinfo_endpoint: 'https://api.github.com/user',
|
|
84
84
|
},
|
|
85
85
|
mapIdentity: (userinfo) => ({
|
|
86
|
-
id: String(userinfo
|
|
87
|
-
name: userinfo
|
|
88
|
-
avatar: userinfo
|
|
89
|
-
email: userinfo
|
|
86
|
+
id: String(userinfo['id'] ?? ''),
|
|
87
|
+
name: userinfo['name'] ?? userinfo['login'] ?? '',
|
|
88
|
+
avatar: userinfo['avatar_url'] ?? undefined,
|
|
89
|
+
email: userinfo['email'] ?? undefined,
|
|
90
90
|
}),
|
|
91
91
|
...opts,
|
|
92
92
|
});
|
|
@@ -144,43 +144,22 @@ export function createAuth0Auth(domain, clientId, opts) {
|
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
// ─── Supauth / GoTrue ────────────────────────────────────────
|
|
147
|
-
function normalizeSupauthIssuer(configuredIssuer) {
|
|
148
|
-
const normalizedIssuer = configuredIssuer.replace(/\/+$/, '');
|
|
149
|
-
if (!normalizedIssuer || normalizedIssuer.endsWith('/auth/v1')) {
|
|
150
|
-
return {
|
|
151
|
-
issuer: normalizedIssuer,
|
|
152
|
-
publicRoot: normalizedIssuer.slice(0, -'/auth/v1'.length),
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
return { issuer: `${normalizedIssuer}/auth/v1`, publicRoot: normalizedIssuer };
|
|
156
|
-
}
|
|
157
147
|
/**
|
|
158
|
-
* Supauth
|
|
148
|
+
* Supauth OIDC Sign-In (GoTrue-compatible identity provider).
|
|
159
149
|
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* `/logout`. Other GoTrue deployments should provide explicit endpoints or use
|
|
163
|
-
* `createSSOAuthProvider()` directly.
|
|
150
|
+
* Compatible with SupaCloud, SupaOAuth, and any GoTrue-based auth backend.
|
|
151
|
+
* Uses standard OIDC Authorization Code Flow with PKCE.
|
|
164
152
|
*
|
|
153
|
+
* @param issuer - Supauth issuer URL (e.g., 'https://auth.example.com')
|
|
165
154
|
* @param clientId - OAuth2 Client ID
|
|
166
|
-
* @param opts - SSO options. `opts.issuer` accepts a Supauth public root or `/auth/v1` issuer URL.
|
|
167
155
|
*/
|
|
168
156
|
export function createSupauthAuth(clientId, opts) {
|
|
169
|
-
const
|
|
170
|
-
const { issuer: configuredIssuer = '', endSessionEndpoint: configuredLogoutEndpoint, storageKey: configuredStorageKey, ...providerOptions } = opts ?? {};
|
|
171
|
-
const { issuer, publicRoot } = normalizeSupauthIssuer(configuredIssuer);
|
|
172
|
-
const storageKey = configuredStorageKey
|
|
173
|
-
?? `svadmin_sso:${encodeURIComponent(configuredIssuer)}:${encodeURIComponent(clientId)}`;
|
|
174
|
-
const endSessionEndpoint = configuredLogoutEndpoint
|
|
175
|
-
?? manualLogoutEndpoint
|
|
176
|
-
?? (publicRoot ? `${publicRoot}/logout` : undefined);
|
|
157
|
+
const issuer = opts?.issuer ?? '';
|
|
177
158
|
return createSSOAuthProvider({
|
|
178
159
|
issuer,
|
|
179
160
|
clientId,
|
|
180
161
|
scopes: ['openid', 'profile', 'email'],
|
|
181
162
|
redirectUri: resolveRedirectUri(opts),
|
|
182
|
-
|
|
183
|
-
...(endSessionEndpoint === undefined ? {} : { endSessionEndpoint }),
|
|
184
|
-
...providerOptions,
|
|
163
|
+
...opts,
|
|
185
164
|
});
|
|
186
165
|
}
|
package/dist/session-manager.js
CHANGED
|
@@ -239,7 +239,7 @@ export function createSessionManager(options) {
|
|
|
239
239
|
function saveSession(session, event) {
|
|
240
240
|
const raw = JSON.stringify(session);
|
|
241
241
|
try {
|
|
242
|
-
//
|
|
242
|
+
// Write the complete token set in one setItem call to avoid partial rotation persistence.
|
|
243
243
|
options.storage.setItem(keys.tokens, raw);
|
|
244
244
|
}
|
|
245
245
|
catch (error) {
|
|
@@ -286,8 +286,8 @@ export function createSessionManager(options) {
|
|
|
286
286
|
}
|
|
287
287
|
function clearTokenSession() {
|
|
288
288
|
const persistenceFailure = clearStorageValue(options.storage, keys.tokens);
|
|
289
|
-
//
|
|
290
|
-
//
|
|
289
|
+
// Token invalidation alone must not cancel a new authentication attempt holding PKCE/state.
|
|
290
|
+
// Explicit logout calls clearSession(), which clears all state and advances the generation.
|
|
291
291
|
const preservesAuthAttempt = hasPendingAuthAttempt();
|
|
292
292
|
if (!preservesAuthAttempt) {
|
|
293
293
|
authGeneration += 1;
|
|
@@ -417,8 +417,8 @@ export function createSessionManager(options) {
|
|
|
417
417
|
}
|
|
418
418
|
throw error;
|
|
419
419
|
}
|
|
420
|
-
//
|
|
421
|
-
//
|
|
420
|
+
// The network request holds only the refresh lock; final CAS and persistence briefly hold the auth lock,
|
|
421
|
+
// linearizing cross-tab logout/callback commits with token rotation.
|
|
422
422
|
return runAuthMutation(async () => {
|
|
423
423
|
const writeRace = resolveRefreshRace(initialRaw);
|
|
424
424
|
if (writeRace.changed)
|
|
@@ -477,7 +477,7 @@ export function createSessionManager(options) {
|
|
|
477
477
|
if (!force && raw === lastObservedRaw)
|
|
478
478
|
return;
|
|
479
479
|
if (raw === null) {
|
|
480
|
-
//
|
|
480
|
+
// Token-only invalidation may race with a new callback; shared state means it must not be treated as logout.
|
|
481
481
|
const shouldPreserveAuthAttempt = preserveAuthAttempt && hasPendingAuthAttempt();
|
|
482
482
|
if (!shouldPreserveAuthAttempt) {
|
|
483
483
|
authGeneration += 1;
|
|
@@ -494,7 +494,7 @@ export function createSessionManager(options) {
|
|
|
494
494
|
syncRemoteChange('SIGNED_OUT', null);
|
|
495
495
|
return;
|
|
496
496
|
}
|
|
497
|
-
//
|
|
497
|
+
// Only a new login in the current context can undo an explicit local sign-out; remote refresh must not resurrect it.
|
|
498
498
|
if (locallySignedOut)
|
|
499
499
|
return;
|
|
500
500
|
authGeneration += 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@svadmin/sso",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OIDC/OAuth2 SSO AuthProvider plugin for svadmin — supports Okta, Azure AD, Cognito, Keycloak, Google",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/vibeunion/svadmin",
|
|
21
|
+
"url": "git+https://github.com/vibeunion/svadmin.git",
|
|
22
22
|
"directory": "packages/sso"
|
|
23
23
|
},
|
|
24
24
|
"sideEffects": false,
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"dist"
|
|
35
35
|
],
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"@svadmin/core": "^0.
|
|
37
|
+
"@svadmin/core": "^0.53.0"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"typescript": "^7.0.2"
|