actor-gate 0.1.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.
- package/package.json +25 -0
- package/src/config/base-config.d.ts +17 -0
- package/src/config/base-config.js +33 -0
- package/src/config/index.d.ts +5 -0
- package/src/config/index.js +5 -0
- package/src/config/nextjs-public-config.d.ts +46 -0
- package/src/config/nextjs-public-config.js +89 -0
- package/src/config/nextjs-server-config.d.ts +32 -0
- package/src/config/nextjs-server-config.js +10 -0
- package/src/config/react-client.d.ts +23 -0
- package/src/config/react-client.js +69 -0
- package/src/config/react-config.d.ts +18 -0
- package/src/config/react-config.js +38 -0
- package/src/core/adapters/access-token-revocation-adapter.d.ts +8 -0
- package/src/core/adapters/access-token-revocation-adapter.js +1 -0
- package/src/core/adapters/access-token-transport-adapter.d.ts +15 -0
- package/src/core/adapters/access-token-transport-adapter.js +1 -0
- package/src/core/adapters/authorization-code-adapter.d.ts +21 -0
- package/src/core/adapters/authorization-code-adapter.js +1 -0
- package/src/core/adapters/authorization-hooks.d.ts +13 -0
- package/src/core/adapters/authorization-hooks.js +1 -0
- package/src/core/adapters/index.d.ts +14 -0
- package/src/core/adapters/index.js +1 -0
- package/src/core/adapters/login-method-adapter.d.ts +7 -0
- package/src/core/adapters/login-method-adapter.js +1 -0
- package/src/core/adapters/oauth-client-adapter.d.ts +13 -0
- package/src/core/adapters/oauth-client-adapter.js +1 -0
- package/src/core/adapters/oauth-client-management-adapter.d.ts +23 -0
- package/src/core/adapters/oauth-client-management-adapter.js +1 -0
- package/src/core/adapters/oauth-grant-type.d.ts +1 -0
- package/src/core/adapters/oauth-grant-type.js +1 -0
- package/src/core/adapters/oauth-policy.d.ts +9 -0
- package/src/core/adapters/oauth-policy.js +1 -0
- package/src/core/adapters/observability-hooks.d.ts +31 -0
- package/src/core/adapters/observability-hooks.js +1 -0
- package/src/core/adapters/pending-auth-request-adapter.d.ts +18 -0
- package/src/core/adapters/pending-auth-request-adapter.js +1 -0
- package/src/core/adapters/refresh-token-adapter.d.ts +24 -0
- package/src/core/adapters/refresh-token-adapter.js +1 -0
- package/src/core/adapters/session-adapter.d.ts +14 -0
- package/src/core/adapters/session-adapter.js +1 -0
- package/src/core/adapters/token-adapter.d.ts +15 -0
- package/src/core/adapters/token-adapter.js +1 -0
- package/src/core/http/bearer-challenge.d.ts +6 -0
- package/src/core/http/bearer-challenge.js +16 -0
- package/src/core/ids/id-codec.d.ts +6 -0
- package/src/core/ids/id-codec.js +30 -0
- package/src/core/index.d.ts +9 -0
- package/src/core/index.js +7 -0
- package/src/core/oauth/pkce.d.ts +9 -0
- package/src/core/oauth/pkce.js +30 -0
- package/src/core/services/access-token-service.d.ts +42 -0
- package/src/core/services/access-token-service.js +304 -0
- package/src/core/services/auth-error.d.ts +14 -0
- package/src/core/services/auth-error.js +47 -0
- package/src/core/services/contracts.d.ts +23 -0
- package/src/core/services/contracts.js +1 -0
- package/src/core/services/direct-auth-service.d.ts +50 -0
- package/src/core/services/direct-auth-service.js +267 -0
- package/src/core/services/index.d.ts +7 -0
- package/src/core/services/index.js +5 -0
- package/src/core/services/mcp-auth-service.d.ts +39 -0
- package/src/core/services/mcp-auth-service.js +170 -0
- package/src/core/services/oauth-service.d.ts +91 -0
- package/src/core/services/oauth-service.js +571 -0
- package/src/core/services/observability.d.ts +22 -0
- package/src/core/services/observability.js +71 -0
- package/src/core/services/revocation-policy.d.ts +21 -0
- package/src/core/services/revocation-policy.js +51 -0
- package/src/core/sessions/client-session.d.ts +7 -0
- package/src/core/sessions/client-session.js +18 -0
- package/src/core/tokens/access-claims.d.ts +21 -0
- package/src/core/tokens/access-claims.js +128 -0
- package/src/core/tokens/id-claims.d.ts +20 -0
- package/src/core/tokens/id-claims.js +25 -0
- package/src/core/types/auth-contract.d.ts +33 -0
- package/src/core/types/auth-contract.js +1 -0
- package/src/express/index.d.ts +1 -0
- package/src/express/index.js +1 -0
- package/src/express/protected-route.d.ts +44 -0
- package/src/express/protected-route.js +119 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +8 -0
- package/src/mcp/index.d.ts +1 -0
- package/src/mcp/index.js +1 -0
- package/src/mcp/json-rpc-auth.d.ts +5 -0
- package/src/mcp/json-rpc-auth.js +41 -0
- package/src/next/app/catch-all.d.ts +32 -0
- package/src/next/app/catch-all.js +82 -0
- package/src/next/app/cookies.d.ts +22 -0
- package/src/next/app/cookies.js +36 -0
- package/src/next/app/direct-auth-handlers.d.ts +55 -0
- package/src/next/app/direct-auth-handlers.js +419 -0
- package/src/next/app/index.d.ts +8 -0
- package/src/next/app/index.js +8 -0
- package/src/next/app/mcp-oauth-handlers.d.ts +74 -0
- package/src/next/app/mcp-oauth-handlers.js +365 -0
- package/src/next/app/protected-route.d.ts +27 -0
- package/src/next/app/protected-route.js +59 -0
- package/src/next/app/request.d.ts +12 -0
- package/src/next/app/request.js +30 -0
- package/src/next/app/response.d.ts +16 -0
- package/src/next/app/response.js +48 -0
- package/src/next/app/wrapper.d.ts +28 -0
- package/src/next/app/wrapper.js +78 -0
- package/src/next/index.d.ts +6 -0
- package/src/next/index.js +5 -0
- package/src/next/pages/catch-all.d.ts +19 -0
- package/src/next/pages/catch-all.js +60 -0
- package/src/next/pages/cookies.d.ts +41 -0
- package/src/next/pages/cookies.js +87 -0
- package/src/next/pages/direct-auth-handlers.d.ts +58 -0
- package/src/next/pages/direct-auth-handlers.js +425 -0
- package/src/next/pages/index.d.ts +8 -0
- package/src/next/pages/index.js +8 -0
- package/src/next/pages/mcp-oauth-handlers.d.ts +77 -0
- package/src/next/pages/mcp-oauth-handlers.js +341 -0
- package/src/next/pages/protected-route.d.ts +28 -0
- package/src/next/pages/protected-route.js +59 -0
- package/src/next/pages/request.d.ts +14 -0
- package/src/next/pages/request.js +66 -0
- package/src/next/pages/response.d.ts +28 -0
- package/src/next/pages/response.js +29 -0
- package/src/next/pages/wrapper.d.ts +29 -0
- package/src/next/pages/wrapper.js +74 -0
- package/src/next/rewrites.d.ts +12 -0
- package/src/next/rewrites.js +74 -0
- package/src/next/shared/auth-http.d.ts +24 -0
- package/src/next/shared/auth-http.js +42 -0
- package/src/next/shared/auth-routes.d.ts +17 -0
- package/src/next/shared/auth-routes.js +153 -0
- package/src/next/shared/direct-auth-utils.d.ts +71 -0
- package/src/next/shared/direct-auth-utils.js +275 -0
- package/src/next/shared/oauth-utils.d.ts +45 -0
- package/src/next/shared/oauth-utils.js +308 -0
- package/src/next/shared/well-known-utils.d.ts +46 -0
- package/src/next/shared/well-known-utils.js +108 -0
- package/src/testing/in-memory/in-memory-access-token-revocation-adapter.d.ts +2 -0
- package/src/testing/in-memory/in-memory-access-token-revocation-adapter.js +14 -0
- package/src/testing/in-memory/in-memory-authorization-code-adapter.d.ts +2 -0
- package/src/testing/in-memory/in-memory-authorization-code-adapter.js +36 -0
- package/src/testing/in-memory/in-memory-oauth-client-adapter.d.ts +14 -0
- package/src/testing/in-memory/in-memory-oauth-client-adapter.js +26 -0
- package/src/testing/in-memory/in-memory-pending-auth-request-adapter.d.ts +2 -0
- package/src/testing/in-memory/in-memory-pending-auth-request-adapter.js +43 -0
- package/src/testing/in-memory/in-memory-refresh-token-adapter.d.ts +2 -0
- package/src/testing/in-memory/in-memory-refresh-token-adapter.js +67 -0
- package/src/testing/in-memory/in-memory-session-adapter.d.ts +6 -0
- package/src/testing/in-memory/in-memory-session-adapter.js +43 -0
- package/src/testing/in-memory/index.d.ts +7 -0
- package/src/testing/in-memory/index.js +7 -0
- package/src/testing/in-memory/test-fixtures.d.ts +5 -0
- package/src/testing/in-memory/test-fixtures.js +18 -0
- package/src/testing/index.d.ts +2 -0
- package/src/testing/index.js +4 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type NextRewrite = {
|
|
2
|
+
source: string;
|
|
3
|
+
destination: string;
|
|
4
|
+
};
|
|
5
|
+
export type BuildAuthRewritesOptions = {
|
|
6
|
+
authBasePath?: string;
|
|
7
|
+
mcpPath?: string;
|
|
8
|
+
authorizePath?: string;
|
|
9
|
+
tokenPath?: string;
|
|
10
|
+
wellKnownBase?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildAuthRewrites(options?: BuildAuthRewritesOptions): NextRewrite[];
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { buildAuthRoutePath } from './shared/auth-routes';
|
|
2
|
+
function normalizePath(path) {
|
|
3
|
+
const trimmed = path.trim();
|
|
4
|
+
if (trimmed.length === 0) {
|
|
5
|
+
throw new Error('Path must be a non-empty string.');
|
|
6
|
+
}
|
|
7
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
8
|
+
if (withLeadingSlash.length === 1) {
|
|
9
|
+
return withLeadingSlash;
|
|
10
|
+
}
|
|
11
|
+
return withLeadingSlash.endsWith('/')
|
|
12
|
+
? withLeadingSlash.slice(0, -1)
|
|
13
|
+
: withLeadingSlash;
|
|
14
|
+
}
|
|
15
|
+
function joinPath(basePath, suffix) {
|
|
16
|
+
const base = normalizePath(basePath);
|
|
17
|
+
const cleanedSuffix = suffix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
18
|
+
if (base === '/') {
|
|
19
|
+
return `/${cleanedSuffix}`;
|
|
20
|
+
}
|
|
21
|
+
return `${base}/${cleanedSuffix}`;
|
|
22
|
+
}
|
|
23
|
+
function assertNoConflictingSource(output, nextRewrite) {
|
|
24
|
+
const existing = output.find(rewrite => rewrite.source === nextRewrite.source);
|
|
25
|
+
if (!existing) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Conflicting rewrite source detected: "${nextRewrite.source}" points to both "${existing.destination}" and "${nextRewrite.destination}".`);
|
|
29
|
+
}
|
|
30
|
+
function pushRewrite(output, source, destination) {
|
|
31
|
+
const normalizedSource = normalizePath(source);
|
|
32
|
+
const normalizedDestination = normalizePath(destination);
|
|
33
|
+
if (normalizedSource === normalizedDestination) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const rewrite = {
|
|
37
|
+
source: normalizedSource,
|
|
38
|
+
destination: normalizedDestination,
|
|
39
|
+
};
|
|
40
|
+
assertNoConflictingSource(output, rewrite);
|
|
41
|
+
output.push(rewrite);
|
|
42
|
+
}
|
|
43
|
+
function buildPublicPathMap(input) {
|
|
44
|
+
return {
|
|
45
|
+
refresh: buildAuthRoutePath('refresh'),
|
|
46
|
+
logout: buildAuthRoutePath('logout'),
|
|
47
|
+
login_start: buildAuthRoutePath('login_start'),
|
|
48
|
+
login_finish: buildAuthRoutePath('login_finish'),
|
|
49
|
+
authorize: input.authorizePath,
|
|
50
|
+
authorize_confirm: joinPath(input.authorizePath, 'confirm'),
|
|
51
|
+
token: input.tokenPath,
|
|
52
|
+
well_known_oauth_protected_resource: joinPath(input.wellKnownBase, 'oauth-protected-resource'),
|
|
53
|
+
well_known_oauth_authorization_server: joinPath(input.wellKnownBase, 'oauth-authorization-server'),
|
|
54
|
+
well_known_openid_configuration: joinPath(input.wellKnownBase, 'openid-configuration'),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export function buildAuthRewrites(options) {
|
|
58
|
+
const authBasePath = normalizePath(options?.authBasePath ?? '/api/auth');
|
|
59
|
+
const authorizePath = normalizePath(options?.authorizePath ?? '/api/authorize');
|
|
60
|
+
const tokenPath = normalizePath(options?.tokenPath ?? '/api/token');
|
|
61
|
+
const wellKnownBase = normalizePath(options?.wellKnownBase ?? '/api/well-known');
|
|
62
|
+
const mcpPath = normalizePath(options?.mcpPath ?? '/api/mcp');
|
|
63
|
+
const publicPaths = buildPublicPathMap({
|
|
64
|
+
authorizePath,
|
|
65
|
+
tokenPath,
|
|
66
|
+
wellKnownBase,
|
|
67
|
+
});
|
|
68
|
+
const rewrites = [];
|
|
69
|
+
Object.keys(publicPaths).forEach(action => {
|
|
70
|
+
pushRewrite(rewrites, publicPaths[action], buildAuthRoutePath(action, { authBasePath }));
|
|
71
|
+
});
|
|
72
|
+
pushRewrite(rewrites, mcpPath, joinPath(authBasePath, 'mcp'));
|
|
73
|
+
return rewrites;
|
|
74
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type AuthServiceError, type AuthServiceErrorCode } from '../../core/services/auth-error';
|
|
2
|
+
export type AuthErrorResponseBody = {
|
|
3
|
+
error: AuthServiceErrorCode | 'system_error';
|
|
4
|
+
error_description: string;
|
|
5
|
+
request_id?: string;
|
|
6
|
+
};
|
|
7
|
+
export type BuiltAuthErrorHttpResponse = {
|
|
8
|
+
statusCode: number;
|
|
9
|
+
challengeHeader?: string;
|
|
10
|
+
body: AuthErrorResponseBody;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildAuthErrorHttpResponse(input: {
|
|
13
|
+
error: AuthServiceError;
|
|
14
|
+
requestId?: string;
|
|
15
|
+
includeChallengeError?: boolean;
|
|
16
|
+
challengeScope?: string;
|
|
17
|
+
resourceMetadataUrl?: string;
|
|
18
|
+
}): BuiltAuthErrorHttpResponse;
|
|
19
|
+
export declare function buildSystemErrorHttpResponse(options?: {
|
|
20
|
+
requestId?: string;
|
|
21
|
+
}): {
|
|
22
|
+
statusCode: 500;
|
|
23
|
+
body: AuthErrorResponseBody;
|
|
24
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { buildBearerChallengeHeader } from '../../core/http/bearer-challenge';
|
|
2
|
+
import { authErrorToHttpStatus, } from '../../core/services/auth-error';
|
|
3
|
+
function toBearerChallengeError(code) {
|
|
4
|
+
return code === 'invalid_client' ? 'invalid_client' : 'invalid_token';
|
|
5
|
+
}
|
|
6
|
+
export function buildAuthErrorHttpResponse(input) {
|
|
7
|
+
const statusCode = authErrorToHttpStatus(input.error.code);
|
|
8
|
+
const challengeHeader = statusCode === 401
|
|
9
|
+
? buildBearerChallengeHeader({
|
|
10
|
+
...(input.challengeScope === undefined
|
|
11
|
+
? {}
|
|
12
|
+
: { scope: input.challengeScope }),
|
|
13
|
+
...(input.resourceMetadataUrl === undefined
|
|
14
|
+
? {}
|
|
15
|
+
: { resourceMetadataUrl: input.resourceMetadataUrl }),
|
|
16
|
+
...(input.includeChallengeError === false
|
|
17
|
+
? {}
|
|
18
|
+
: { error: toBearerChallengeError(input.error.code) }),
|
|
19
|
+
})
|
|
20
|
+
: undefined;
|
|
21
|
+
return {
|
|
22
|
+
statusCode,
|
|
23
|
+
...(challengeHeader === undefined ? {} : { challengeHeader }),
|
|
24
|
+
body: {
|
|
25
|
+
error: input.error.code,
|
|
26
|
+
error_description: input.error.message,
|
|
27
|
+
...(input.requestId === undefined ? {} : { request_id: input.requestId }),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function buildSystemErrorHttpResponse(options) {
|
|
32
|
+
return {
|
|
33
|
+
statusCode: 500,
|
|
34
|
+
body: {
|
|
35
|
+
error: 'system_error',
|
|
36
|
+
error_description: 'Internal server error.',
|
|
37
|
+
...(options?.requestId === undefined
|
|
38
|
+
? {}
|
|
39
|
+
: { request_id: options.requestId }),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const AUTH_ROUTE_ACTIONS: readonly ["refresh", "logout", "login_start", "login_finish", "authorize", "authorize_confirm", "token", "well_known_oauth_protected_resource", "well_known_oauth_authorization_server", "well_known_openid_configuration"];
|
|
2
|
+
export type AuthRouteAction = (typeof AUTH_ROUTE_ACTIONS)[number];
|
|
3
|
+
export declare const AUTH_ROUTE_HTTP_METHODS: readonly ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"];
|
|
4
|
+
export type AuthRouteHttpMethod = (typeof AUTH_ROUTE_HTTP_METHODS)[number];
|
|
5
|
+
export type AuthRouteDefinition = {
|
|
6
|
+
action: AuthRouteAction;
|
|
7
|
+
segments: readonly string[];
|
|
8
|
+
aliases?: readonly (readonly string[])[];
|
|
9
|
+
methods: readonly AuthRouteHttpMethod[];
|
|
10
|
+
};
|
|
11
|
+
export declare const AUTH_ROUTE_DEFINITIONS: readonly AuthRouteDefinition[];
|
|
12
|
+
export declare function normalizeAuthRouteSegments(value: string | readonly string[] | undefined): string[];
|
|
13
|
+
export declare function resolveAuthRoute(segments: readonly string[]): AuthRouteDefinition | undefined;
|
|
14
|
+
export declare function isAuthRouteMethodAllowed(route: AuthRouteDefinition, method: string | undefined): boolean;
|
|
15
|
+
export declare function buildAuthRoutePath(action: AuthRouteAction, options?: {
|
|
16
|
+
authBasePath?: string;
|
|
17
|
+
}): string;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export const AUTH_ROUTE_ACTIONS = [
|
|
2
|
+
'refresh',
|
|
3
|
+
'logout',
|
|
4
|
+
'login_start',
|
|
5
|
+
'login_finish',
|
|
6
|
+
'authorize',
|
|
7
|
+
'authorize_confirm',
|
|
8
|
+
'token',
|
|
9
|
+
'well_known_oauth_protected_resource',
|
|
10
|
+
'well_known_oauth_authorization_server',
|
|
11
|
+
'well_known_openid_configuration',
|
|
12
|
+
];
|
|
13
|
+
export const AUTH_ROUTE_HTTP_METHODS = [
|
|
14
|
+
'GET',
|
|
15
|
+
'POST',
|
|
16
|
+
'PUT',
|
|
17
|
+
'PATCH',
|
|
18
|
+
'DELETE',
|
|
19
|
+
'OPTIONS',
|
|
20
|
+
'HEAD',
|
|
21
|
+
];
|
|
22
|
+
const AUTH_ROUTE_DEFINITION_MAP = {
|
|
23
|
+
refresh: {
|
|
24
|
+
action: 'refresh',
|
|
25
|
+
segments: ['refresh'],
|
|
26
|
+
methods: ['POST'],
|
|
27
|
+
},
|
|
28
|
+
logout: {
|
|
29
|
+
action: 'logout',
|
|
30
|
+
segments: ['logout'],
|
|
31
|
+
methods: ['POST'],
|
|
32
|
+
},
|
|
33
|
+
login_start: {
|
|
34
|
+
action: 'login_start',
|
|
35
|
+
segments: ['login', 'start'],
|
|
36
|
+
aliases: [['magic', 'start']],
|
|
37
|
+
methods: ['POST'],
|
|
38
|
+
},
|
|
39
|
+
login_finish: {
|
|
40
|
+
action: 'login_finish',
|
|
41
|
+
segments: ['login', 'finish'],
|
|
42
|
+
aliases: [['magic', 'verify']],
|
|
43
|
+
methods: ['GET'],
|
|
44
|
+
},
|
|
45
|
+
authorize: {
|
|
46
|
+
action: 'authorize',
|
|
47
|
+
segments: ['authorize'],
|
|
48
|
+
methods: ['GET'],
|
|
49
|
+
},
|
|
50
|
+
authorize_confirm: {
|
|
51
|
+
action: 'authorize_confirm',
|
|
52
|
+
segments: ['authorize', 'confirm'],
|
|
53
|
+
methods: ['POST'],
|
|
54
|
+
},
|
|
55
|
+
token: {
|
|
56
|
+
action: 'token',
|
|
57
|
+
segments: ['token'],
|
|
58
|
+
methods: ['POST'],
|
|
59
|
+
},
|
|
60
|
+
well_known_oauth_protected_resource: {
|
|
61
|
+
action: 'well_known_oauth_protected_resource',
|
|
62
|
+
segments: ['well-known', 'oauth-protected-resource'],
|
|
63
|
+
methods: ['GET'],
|
|
64
|
+
},
|
|
65
|
+
well_known_oauth_authorization_server: {
|
|
66
|
+
action: 'well_known_oauth_authorization_server',
|
|
67
|
+
segments: ['well-known', 'oauth-authorization-server'],
|
|
68
|
+
methods: ['GET'],
|
|
69
|
+
},
|
|
70
|
+
well_known_openid_configuration: {
|
|
71
|
+
action: 'well_known_openid_configuration',
|
|
72
|
+
segments: ['well-known', 'openid-configuration'],
|
|
73
|
+
methods: ['GET'],
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
function safeDecodeUriComponent(value) {
|
|
77
|
+
try {
|
|
78
|
+
return decodeURIComponent(value);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function normalizePathPart(part) {
|
|
85
|
+
return part
|
|
86
|
+
.split('/')
|
|
87
|
+
.map(segment => segment.trim())
|
|
88
|
+
.filter(segment => segment.length > 0)
|
|
89
|
+
.map(safeDecodeUriComponent);
|
|
90
|
+
}
|
|
91
|
+
function areSegmentsEqual(left, right) {
|
|
92
|
+
if (left.length !== right.length) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
96
|
+
if (left[index] !== right[index]) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
function normalizePathPrefix(path) {
|
|
103
|
+
const trimmed = path.trim();
|
|
104
|
+
if (trimmed.length === 0) {
|
|
105
|
+
throw new Error('Path must be a non-empty string.');
|
|
106
|
+
}
|
|
107
|
+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
108
|
+
if (withLeadingSlash.length === 1) {
|
|
109
|
+
return withLeadingSlash;
|
|
110
|
+
}
|
|
111
|
+
return withLeadingSlash.endsWith('/')
|
|
112
|
+
? withLeadingSlash.slice(0, -1)
|
|
113
|
+
: withLeadingSlash;
|
|
114
|
+
}
|
|
115
|
+
export const AUTH_ROUTE_DEFINITIONS = AUTH_ROUTE_ACTIONS.map(action => AUTH_ROUTE_DEFINITION_MAP[action]);
|
|
116
|
+
export function normalizeAuthRouteSegments(value) {
|
|
117
|
+
if (value === undefined) {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
const parts = Array.isArray(value) ? value : [value];
|
|
121
|
+
const segments = [];
|
|
122
|
+
for (const part of parts) {
|
|
123
|
+
segments.push(...normalizePathPart(part));
|
|
124
|
+
}
|
|
125
|
+
return segments;
|
|
126
|
+
}
|
|
127
|
+
export function resolveAuthRoute(segments) {
|
|
128
|
+
for (const route of AUTH_ROUTE_DEFINITIONS) {
|
|
129
|
+
if (areSegmentsEqual(route.segments, segments)) {
|
|
130
|
+
return route;
|
|
131
|
+
}
|
|
132
|
+
if (route.aliases) {
|
|
133
|
+
for (const alias of route.aliases) {
|
|
134
|
+
if (areSegmentsEqual(alias, segments)) {
|
|
135
|
+
return route;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
export function isAuthRouteMethodAllowed(route, method) {
|
|
143
|
+
if (!method) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return route.methods.includes(method.toUpperCase());
|
|
147
|
+
}
|
|
148
|
+
export function buildAuthRoutePath(action, options) {
|
|
149
|
+
const basePath = normalizePathPrefix(options?.authBasePath ?? '/api/auth');
|
|
150
|
+
const route = AUTH_ROUTE_DEFINITION_MAP[action];
|
|
151
|
+
const routeSuffix = route.segments.join('/');
|
|
152
|
+
return basePath === '/' ? `/${routeSuffix}` : `${basePath}/${routeSuffix}`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AccessTokenSource, AccessTokenTransportAdapter, AccessTokenTransportInput } from '../../core/adapters/access-token-transport-adapter';
|
|
2
|
+
import type { AuthActor } from '../../core/types/auth-contract';
|
|
3
|
+
export declare const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = "access_token";
|
|
4
|
+
export declare const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = "refresh_token";
|
|
5
|
+
export declare const DEFAULT_CSRF_COOKIE_NAME = "csrf_token";
|
|
6
|
+
export declare const DEFAULT_CSRF_HEADER_NAME = "x-csrf-token";
|
|
7
|
+
export declare const DEFAULT_CSRF_BODY_FIELD_NAME = "csrf_token";
|
|
8
|
+
export type DirectAccessTokenTransportConfig<TActor extends AuthActor = AuthActor> = {
|
|
9
|
+
priority?: readonly AccessTokenSource[];
|
|
10
|
+
cookieName?: string;
|
|
11
|
+
requireBearerForActors?: readonly TActor[] | ReadonlySet<TActor>;
|
|
12
|
+
adapter?: AccessTokenTransportAdapter<TActor>;
|
|
13
|
+
};
|
|
14
|
+
export type DirectCsrfConfig = {
|
|
15
|
+
enabled?: boolean;
|
|
16
|
+
headerName?: string;
|
|
17
|
+
cookieName?: string;
|
|
18
|
+
bodyFieldName?: string;
|
|
19
|
+
ignorePaths?: readonly string[];
|
|
20
|
+
};
|
|
21
|
+
export type RefreshTokenSource = 'cookie' | 'body';
|
|
22
|
+
export type HeaderBag = Headers | Record<string, unknown>;
|
|
23
|
+
export declare function parseBearerAuthorizationHeader(authorizationHeader: string | undefined): string | undefined;
|
|
24
|
+
export declare function extractAccessTokenWithPriority(input: {
|
|
25
|
+
transport: AccessTokenTransportInput;
|
|
26
|
+
priority?: readonly AccessTokenSource[];
|
|
27
|
+
cookieName?: string;
|
|
28
|
+
}): {
|
|
29
|
+
token: string | null;
|
|
30
|
+
source?: AccessTokenSource;
|
|
31
|
+
};
|
|
32
|
+
export declare function resolveAccessTokenTransportAdapter<TActor extends AuthActor = AuthActor>(config: DirectAccessTokenTransportConfig<TActor> | undefined): AccessTokenTransportAdapter<TActor>;
|
|
33
|
+
export declare function assertBearerOnlyActorPolicy<TActor extends AuthActor>(input: {
|
|
34
|
+
source?: AccessTokenSource;
|
|
35
|
+
actor: TActor;
|
|
36
|
+
requireBearerForActors?: readonly TActor[] | ReadonlySet<TActor>;
|
|
37
|
+
}): void;
|
|
38
|
+
export declare function extractRefreshToken(input: {
|
|
39
|
+
body: Record<string, unknown>;
|
|
40
|
+
cookies: Record<string, string | undefined>;
|
|
41
|
+
cookieName?: string;
|
|
42
|
+
bodyFieldName?: string;
|
|
43
|
+
priority?: readonly RefreshTokenSource[];
|
|
44
|
+
}): {
|
|
45
|
+
token?: string;
|
|
46
|
+
source?: RefreshTokenSource;
|
|
47
|
+
};
|
|
48
|
+
export declare function getHeaderValue(headers: HeaderBag, headerName: string): string | undefined;
|
|
49
|
+
export declare function resolvePathnameFromUrl(url: string | undefined): string;
|
|
50
|
+
export declare function isMutationHttpMethod(method: string | undefined): boolean;
|
|
51
|
+
export declare function assertCsrfForCookieMutation(input: {
|
|
52
|
+
method: string | undefined;
|
|
53
|
+
pathname: string;
|
|
54
|
+
cookies: Record<string, string | undefined>;
|
|
55
|
+
headers: HeaderBag;
|
|
56
|
+
body?: Record<string, unknown>;
|
|
57
|
+
cookieAuthenticated: boolean;
|
|
58
|
+
config?: DirectCsrfConfig;
|
|
59
|
+
}): void;
|
|
60
|
+
export declare function resolveLoginMethod(input: {
|
|
61
|
+
pathname: string;
|
|
62
|
+
phase: 'start' | 'finish';
|
|
63
|
+
body?: Record<string, unknown>;
|
|
64
|
+
queryMethod?: unknown;
|
|
65
|
+
defaultMethod?: string;
|
|
66
|
+
}): string;
|
|
67
|
+
export declare function resolveCookieSecureFlag(input?: {
|
|
68
|
+
secure?: boolean;
|
|
69
|
+
secureInProduction?: boolean;
|
|
70
|
+
}): boolean;
|
|
71
|
+
export declare function parseNonNegativeSafeInteger(value: unknown, fieldName: string): number;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { AuthServiceError } from '../../core/services/auth-error';
|
|
2
|
+
import { toSingleString } from './oauth-utils';
|
|
3
|
+
export const DEFAULT_ACCESS_TOKEN_COOKIE_NAME = 'access_token';
|
|
4
|
+
export const DEFAULT_REFRESH_TOKEN_COOKIE_NAME = 'refresh_token';
|
|
5
|
+
export const DEFAULT_CSRF_COOKIE_NAME = 'csrf_token';
|
|
6
|
+
export const DEFAULT_CSRF_HEADER_NAME = 'x-csrf-token';
|
|
7
|
+
export const DEFAULT_CSRF_BODY_FIELD_NAME = 'csrf_token';
|
|
8
|
+
const DEFAULT_ACCESS_TOKEN_PRIORITY = [
|
|
9
|
+
'bearer',
|
|
10
|
+
'cookie',
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_REFRESH_TOKEN_PRIORITY = [
|
|
13
|
+
'cookie',
|
|
14
|
+
'body',
|
|
15
|
+
];
|
|
16
|
+
function normalizeAccessTokenPriority(priority) {
|
|
17
|
+
const resolved = priority ?? DEFAULT_ACCESS_TOKEN_PRIORITY;
|
|
18
|
+
if (resolved.length === 0) {
|
|
19
|
+
throw new AuthServiceError({
|
|
20
|
+
code: 'invalid_request',
|
|
21
|
+
message: 'accessToken priority must contain at least one source.',
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const normalized = [];
|
|
25
|
+
for (const source of resolved) {
|
|
26
|
+
if (source !== 'bearer' && source !== 'cookie') {
|
|
27
|
+
throw new AuthServiceError({
|
|
28
|
+
code: 'invalid_request',
|
|
29
|
+
message: `Unsupported accessToken priority source "${String(source)}".`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
normalized.push(source);
|
|
33
|
+
}
|
|
34
|
+
return normalized;
|
|
35
|
+
}
|
|
36
|
+
function normalizeRefreshTokenPriority(priority) {
|
|
37
|
+
const resolved = priority ?? DEFAULT_REFRESH_TOKEN_PRIORITY;
|
|
38
|
+
if (resolved.length === 0) {
|
|
39
|
+
throw new AuthServiceError({
|
|
40
|
+
code: 'invalid_request',
|
|
41
|
+
message: 'refreshToken priority must contain at least one source.',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const normalized = [];
|
|
45
|
+
for (const source of resolved) {
|
|
46
|
+
if (source !== 'cookie' && source !== 'body') {
|
|
47
|
+
throw new AuthServiceError({
|
|
48
|
+
code: 'invalid_request',
|
|
49
|
+
message: `Unsupported refreshToken priority source "${String(source)}".`,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
normalized.push(source);
|
|
53
|
+
}
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
function resolveCookieName(value, fallbackName) {
|
|
57
|
+
const normalized = toSingleString(value);
|
|
58
|
+
if (normalized === undefined) {
|
|
59
|
+
return fallbackName;
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
function toReadonlyActorSet(actors) {
|
|
64
|
+
if (!actors) {
|
|
65
|
+
return new Set();
|
|
66
|
+
}
|
|
67
|
+
if (actors instanceof Set) {
|
|
68
|
+
return actors;
|
|
69
|
+
}
|
|
70
|
+
return new Set(actors);
|
|
71
|
+
}
|
|
72
|
+
export function parseBearerAuthorizationHeader(authorizationHeader) {
|
|
73
|
+
if (!authorizationHeader) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const match = authorizationHeader.match(/^Bearer\s+(.+)$/i);
|
|
77
|
+
if (!match) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const token = match[1].trim();
|
|
81
|
+
return token.length > 0 ? token : undefined;
|
|
82
|
+
}
|
|
83
|
+
export function extractAccessTokenWithPriority(input) {
|
|
84
|
+
const priority = normalizeAccessTokenPriority(input.priority);
|
|
85
|
+
const cookieName = resolveCookieName(input.cookieName, DEFAULT_ACCESS_TOKEN_COOKIE_NAME);
|
|
86
|
+
for (const source of priority) {
|
|
87
|
+
if (source === 'bearer') {
|
|
88
|
+
const token = parseBearerAuthorizationHeader(input.transport.authorizationHeader);
|
|
89
|
+
if (token) {
|
|
90
|
+
return { token, source };
|
|
91
|
+
}
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const cookieToken = toSingleString(input.transport.cookies?.[cookieName]);
|
|
95
|
+
if (cookieToken) {
|
|
96
|
+
return {
|
|
97
|
+
token: cookieToken,
|
|
98
|
+
source,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { token: null };
|
|
103
|
+
}
|
|
104
|
+
export function resolveAccessTokenTransportAdapter(config) {
|
|
105
|
+
if (config?.adapter) {
|
|
106
|
+
return config.adapter;
|
|
107
|
+
}
|
|
108
|
+
const priority = normalizeAccessTokenPriority(config?.priority);
|
|
109
|
+
const cookieName = resolveCookieName(config?.cookieName, DEFAULT_ACCESS_TOKEN_COOKIE_NAME);
|
|
110
|
+
return {
|
|
111
|
+
extractAccessToken: ({ transport }) => extractAccessTokenWithPriority({
|
|
112
|
+
transport,
|
|
113
|
+
priority,
|
|
114
|
+
cookieName,
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function assertBearerOnlyActorPolicy(input) {
|
|
119
|
+
if (input.source !== 'cookie') {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const actorSet = toReadonlyActorSet(input.requireBearerForActors);
|
|
123
|
+
if (!actorSet.has(input.actor)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
throw new AuthServiceError({
|
|
127
|
+
code: 'forbidden',
|
|
128
|
+
message: `Actor "${input.actor}" requires bearer token transport.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
export function extractRefreshToken(input) {
|
|
132
|
+
const cookieName = resolveCookieName(input.cookieName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
133
|
+
const bodyFieldName = resolveCookieName(input.bodyFieldName, DEFAULT_REFRESH_TOKEN_COOKIE_NAME);
|
|
134
|
+
const priority = normalizeRefreshTokenPriority(input.priority);
|
|
135
|
+
for (const source of priority) {
|
|
136
|
+
if (source === 'cookie') {
|
|
137
|
+
const token = toSingleString(input.cookies[cookieName]);
|
|
138
|
+
if (token) {
|
|
139
|
+
return { token, source };
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const token = toSingleString(input.body[bodyFieldName]);
|
|
144
|
+
if (token) {
|
|
145
|
+
return { token, source };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
export function getHeaderValue(headers, headerName) {
|
|
151
|
+
if (headers instanceof Headers) {
|
|
152
|
+
const value = headers.get(headerName);
|
|
153
|
+
return value === null ? undefined : toSingleString(value);
|
|
154
|
+
}
|
|
155
|
+
const lowerHeaderName = headerName.toLowerCase();
|
|
156
|
+
const direct = headers[headerName] ??
|
|
157
|
+
headers[lowerHeaderName] ??
|
|
158
|
+
headers[headerName.toUpperCase()];
|
|
159
|
+
if (direct !== undefined) {
|
|
160
|
+
return toSingleString(direct);
|
|
161
|
+
}
|
|
162
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
163
|
+
if (key.toLowerCase() === lowerHeaderName) {
|
|
164
|
+
return toSingleString(value);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
export function resolvePathnameFromUrl(url) {
|
|
170
|
+
if (!url || url.trim().length === 0) {
|
|
171
|
+
return '/';
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const parsed = new URL(url, 'http://localhost');
|
|
175
|
+
return parsed.pathname;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return '/';
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function pathMatches(pathname, pathPrefix) {
|
|
182
|
+
const normalizedPath = pathPrefix.trim();
|
|
183
|
+
if (normalizedPath.length === 0) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const prefix = normalizedPath.startsWith('/')
|
|
187
|
+
? normalizedPath
|
|
188
|
+
: `/${normalizedPath}`;
|
|
189
|
+
if (pathname === prefix) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return pathname.startsWith(`${prefix}/`);
|
|
193
|
+
}
|
|
194
|
+
export function isMutationHttpMethod(method) {
|
|
195
|
+
if (!method) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const normalized = method.toUpperCase();
|
|
199
|
+
return (normalized !== 'GET' && normalized !== 'HEAD' && normalized !== 'OPTIONS');
|
|
200
|
+
}
|
|
201
|
+
export function assertCsrfForCookieMutation(input) {
|
|
202
|
+
const enabled = input.config?.enabled ?? true;
|
|
203
|
+
if (!enabled || !input.cookieAuthenticated) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!isMutationHttpMethod(input.method)) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (input.config?.ignorePaths?.some(path => pathMatches(input.pathname, path))) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const cookieName = resolveCookieName(input.config?.cookieName, DEFAULT_CSRF_COOKIE_NAME);
|
|
213
|
+
const headerName = resolveCookieName(input.config?.headerName, DEFAULT_CSRF_HEADER_NAME);
|
|
214
|
+
const bodyFieldName = resolveCookieName(input.config?.bodyFieldName, DEFAULT_CSRF_BODY_FIELD_NAME);
|
|
215
|
+
const expected = toSingleString(input.cookies[cookieName]);
|
|
216
|
+
const provided = getHeaderValue(input.headers, headerName) ??
|
|
217
|
+
toSingleString(input.body?.[bodyFieldName]);
|
|
218
|
+
if (!expected || !provided || expected !== provided) {
|
|
219
|
+
throw new AuthServiceError({
|
|
220
|
+
code: 'forbidden',
|
|
221
|
+
message: 'CSRF token validation failed.',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function inferLoginMethodFromPath(input) {
|
|
226
|
+
const parts = input.pathname.split('/').filter(part => part.length > 0);
|
|
227
|
+
if (parts.length < 2) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const actionPart = parts[parts.length - 1];
|
|
231
|
+
const methodPart = parts[parts.length - 2];
|
|
232
|
+
if (input.phase === 'start') {
|
|
233
|
+
if (actionPart !== 'start') {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
return methodPart === 'login' ? undefined : methodPart;
|
|
237
|
+
}
|
|
238
|
+
if (actionPart !== 'finish' && actionPart !== 'verify') {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
return methodPart === 'login' ? undefined : methodPart;
|
|
242
|
+
}
|
|
243
|
+
export function resolveLoginMethod(input) {
|
|
244
|
+
const fromPath = inferLoginMethodFromPath(input);
|
|
245
|
+
const fromBody = toSingleString(input.body?.method);
|
|
246
|
+
const fromQuery = toSingleString(input.queryMethod);
|
|
247
|
+
const fromDefault = toSingleString(input.defaultMethod);
|
|
248
|
+
const method = fromPath ?? fromBody ?? fromQuery ?? fromDefault;
|
|
249
|
+
if (method === undefined) {
|
|
250
|
+
throw new AuthServiceError({
|
|
251
|
+
code: 'invalid_request',
|
|
252
|
+
message: 'Missing required field "method".',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return method;
|
|
256
|
+
}
|
|
257
|
+
export function resolveCookieSecureFlag(input) {
|
|
258
|
+
if (input?.secure !== undefined) {
|
|
259
|
+
return input.secure;
|
|
260
|
+
}
|
|
261
|
+
const secureInProduction = input?.secureInProduction ?? true;
|
|
262
|
+
return secureInProduction && process.env.NODE_ENV === 'production';
|
|
263
|
+
}
|
|
264
|
+
export function parseNonNegativeSafeInteger(value, fieldName) {
|
|
265
|
+
const normalized = typeof value === 'number'
|
|
266
|
+
? value
|
|
267
|
+
: Number.parseInt(toSingleString(value) ?? '', 10);
|
|
268
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
269
|
+
throw new AuthServiceError({
|
|
270
|
+
code: 'invalid_request',
|
|
271
|
+
message: `${fieldName} must be a non-negative safe integer.`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
return normalized;
|
|
275
|
+
}
|