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,51 @@
|
|
|
1
|
+
export function shouldValidateSession(input) {
|
|
2
|
+
if (input.sessionMode === 'stateless') {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
if (input.sessionMode === 'stateful') {
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
return input.hasSessionAdapter;
|
|
9
|
+
}
|
|
10
|
+
export function shouldCheckAccessTokenRevocation(input) {
|
|
11
|
+
if (!input.hasRevocationAdapter) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
return input.revocationGuarantee !== 'ttl_only';
|
|
15
|
+
}
|
|
16
|
+
function shouldUseBoundedCache(input) {
|
|
17
|
+
return (input.nowUnix - input.entry.checkedAtUnix <
|
|
18
|
+
input.boundedRevocationCacheSeconds);
|
|
19
|
+
}
|
|
20
|
+
export function createRevocationDecisionEngine() {
|
|
21
|
+
const boundedCache = new Map();
|
|
22
|
+
return {
|
|
23
|
+
async isAccessTokenRevoked({ jti, nowUnix, revocationGuarantee, boundedRevocationCacheSeconds, accessTokenRevocationAdapter, }) {
|
|
24
|
+
if (!shouldCheckAccessTokenRevocation({
|
|
25
|
+
revocationGuarantee,
|
|
26
|
+
hasRevocationAdapter: accessTokenRevocationAdapter !== undefined,
|
|
27
|
+
}) ||
|
|
28
|
+
!accessTokenRevocationAdapter) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (revocationGuarantee === 'strict') {
|
|
32
|
+
return accessTokenRevocationAdapter.isRevoked(jti);
|
|
33
|
+
}
|
|
34
|
+
const cached = boundedCache.get(jti);
|
|
35
|
+
if (cached &&
|
|
36
|
+
shouldUseBoundedCache({
|
|
37
|
+
nowUnix,
|
|
38
|
+
boundedRevocationCacheSeconds,
|
|
39
|
+
entry: cached,
|
|
40
|
+
})) {
|
|
41
|
+
return cached.revoked;
|
|
42
|
+
}
|
|
43
|
+
const revoked = await accessTokenRevocationAdapter.isRevoked(jti);
|
|
44
|
+
boundedCache.set(jti, {
|
|
45
|
+
checkedAtUnix: nowUnix,
|
|
46
|
+
revoked,
|
|
47
|
+
});
|
|
48
|
+
return revoked;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AuthActor, ClientSession, SessionRecord } from '../types/auth-contract';
|
|
2
|
+
export declare function buildClientSession<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>>(input: {
|
|
3
|
+
session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
4
|
+
buildClientSessionData?: (input: {
|
|
5
|
+
session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
6
|
+
}) => TClientSessionData;
|
|
7
|
+
}): ClientSession<TSessionId, TUserId, TActor, TClientSessionData>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function buildClientSession(input) {
|
|
2
|
+
const clientSession = {
|
|
3
|
+
sessionId: input.session.sessionId,
|
|
4
|
+
userId: input.session.userId,
|
|
5
|
+
actor: input.session.actor,
|
|
6
|
+
issuedAt: input.session.issuedAt,
|
|
7
|
+
expiresAt: input.session.expiresAt,
|
|
8
|
+
};
|
|
9
|
+
if (input.session.clientId !== undefined) {
|
|
10
|
+
clientSession.clientId = input.session.clientId;
|
|
11
|
+
}
|
|
12
|
+
if (input.buildClientSessionData) {
|
|
13
|
+
clientSession.clientSessionData = input.buildClientSessionData({
|
|
14
|
+
session: input.session,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return clientSession;
|
|
18
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IdCodec } from '../ids/id-codec';
|
|
2
|
+
import type { AccessClaims, AuthActor, SessionRecord } from '../types/auth-contract';
|
|
3
|
+
export declare const DEFAULT_TOKEN_CLAIMS_VERSION = 1;
|
|
4
|
+
export type BuildAccessClaimsInput<TSessionId, TUserId, TActor extends AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
|
|
5
|
+
session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
6
|
+
sessionIdCodec: IdCodec<TSessionId>;
|
|
7
|
+
userIdCodec: IdCodec<TUserId>;
|
|
8
|
+
audience: string;
|
|
9
|
+
accessTokenTtlSeconds: number;
|
|
10
|
+
tokenClaimsVersion?: number;
|
|
11
|
+
nowUnix?: number;
|
|
12
|
+
jtiFactory?: () => string;
|
|
13
|
+
buildExtClaims?: (input: {
|
|
14
|
+
session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
15
|
+
}) => TExtClaims;
|
|
16
|
+
};
|
|
17
|
+
export declare function buildAccessClaims<TSessionId, TUserId, TActor extends AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>>(input: BuildAccessClaimsInput<TSessionId, TUserId, TActor, TServerSessionData, TExtClaims>): AccessClaims<TActor, TExtClaims>;
|
|
18
|
+
export declare function parseAccessClaims<TActor extends AuthActor = AuthActor, TExtClaims extends Record<string, unknown> = Record<string, never>>(input: {
|
|
19
|
+
claims: unknown;
|
|
20
|
+
tokenClaimsVersion?: number;
|
|
21
|
+
}): AccessClaims<TActor, TExtClaims>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { encodeSubjectClaims } from './id-claims';
|
|
3
|
+
export const DEFAULT_TOKEN_CLAIMS_VERSION = 1;
|
|
4
|
+
const ALLOWED_TOP_LEVEL_CLAIM_KEYS = new Set([
|
|
5
|
+
'sub',
|
|
6
|
+
'uid',
|
|
7
|
+
'actor',
|
|
8
|
+
'cid',
|
|
9
|
+
'ver',
|
|
10
|
+
'jti',
|
|
11
|
+
'ext',
|
|
12
|
+
'iat',
|
|
13
|
+
'exp',
|
|
14
|
+
'aud',
|
|
15
|
+
]);
|
|
16
|
+
function assertPositiveSafeInteger(value, fieldName) {
|
|
17
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
18
|
+
throw new Error(`${fieldName} must be a positive safe integer.`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function readRequiredStringClaim(claims, claimName) {
|
|
22
|
+
const value = claims[claimName];
|
|
23
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
24
|
+
throw new Error(`Invalid or missing "${claimName}" claim.`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function readOptionalStringClaim(claims, claimName) {
|
|
29
|
+
const value = claims[claimName];
|
|
30
|
+
if (value === undefined) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
34
|
+
throw new Error(`Invalid "${claimName}" claim.`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
function readRequiredPositiveSafeIntegerClaim(claims, claimName) {
|
|
39
|
+
const value = claims[claimName];
|
|
40
|
+
if (typeof value !== 'number') {
|
|
41
|
+
throw new Error(`Invalid or missing "${claimName}" claim.`);
|
|
42
|
+
}
|
|
43
|
+
assertPositiveSafeInteger(value, claimName);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function assertOnlyExpectedTopLevelClaims(claims) {
|
|
47
|
+
for (const key of Object.keys(claims)) {
|
|
48
|
+
if (!ALLOWED_TOP_LEVEL_CLAIM_KEYS.has(key)) {
|
|
49
|
+
throw new Error(`Unexpected top-level claim "${key}".`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function buildAccessClaims(input) {
|
|
54
|
+
if (input.audience.length === 0) {
|
|
55
|
+
throw new Error('audience must be a non-empty string.');
|
|
56
|
+
}
|
|
57
|
+
assertPositiveSafeInteger(input.accessTokenTtlSeconds, 'accessTokenTtlSeconds');
|
|
58
|
+
const version = input.tokenClaimsVersion ?? DEFAULT_TOKEN_CLAIMS_VERSION;
|
|
59
|
+
assertPositiveSafeInteger(version, 'tokenClaimsVersion');
|
|
60
|
+
const issuedAtUnix = input.nowUnix ?? Math.floor(Date.now() / 1000);
|
|
61
|
+
assertPositiveSafeInteger(issuedAtUnix, 'nowUnix');
|
|
62
|
+
const expiresAtUnix = issuedAtUnix + input.accessTokenTtlSeconds;
|
|
63
|
+
assertPositiveSafeInteger(expiresAtUnix, 'exp');
|
|
64
|
+
const jtiFactory = input.jtiFactory ?? (() => randomUUID());
|
|
65
|
+
const jti = jtiFactory();
|
|
66
|
+
if (jti.length === 0) {
|
|
67
|
+
throw new Error('jti must be a non-empty string.');
|
|
68
|
+
}
|
|
69
|
+
const subjectClaims = encodeSubjectClaims({
|
|
70
|
+
sessionId: input.session.sessionId,
|
|
71
|
+
userId: input.session.userId,
|
|
72
|
+
sessionIdCodec: input.sessionIdCodec,
|
|
73
|
+
userIdCodec: input.userIdCodec,
|
|
74
|
+
});
|
|
75
|
+
const extClaims = input.buildExtClaims?.({ session: input.session });
|
|
76
|
+
return {
|
|
77
|
+
...subjectClaims,
|
|
78
|
+
actor: input.session.actor,
|
|
79
|
+
...(input.session.clientId === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { cid: input.session.clientId }),
|
|
82
|
+
ver: version,
|
|
83
|
+
jti,
|
|
84
|
+
...(extClaims === undefined ? {} : { ext: extClaims }),
|
|
85
|
+
iat: issuedAtUnix,
|
|
86
|
+
exp: expiresAtUnix,
|
|
87
|
+
aud: input.audience,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function parseAccessClaims(input) {
|
|
91
|
+
// Actor allowlist enforcement is intentionally handled by higher-level
|
|
92
|
+
// services using bootstrap config (default reserved actor: 'USER', plus any
|
|
93
|
+
// app-provided actors). This function validates claim shape only.
|
|
94
|
+
if (!input.claims || typeof input.claims !== 'object') {
|
|
95
|
+
throw new Error('Token claims payload must be an object.');
|
|
96
|
+
}
|
|
97
|
+
const claims = input.claims;
|
|
98
|
+
assertOnlyExpectedTopLevelClaims(claims);
|
|
99
|
+
const version = readRequiredPositiveSafeIntegerClaim(claims, 'ver');
|
|
100
|
+
const expectedVersion = input.tokenClaimsVersion ?? DEFAULT_TOKEN_CLAIMS_VERSION;
|
|
101
|
+
assertPositiveSafeInteger(expectedVersion, 'tokenClaimsVersion');
|
|
102
|
+
if (version !== expectedVersion) {
|
|
103
|
+
throw new Error(`Unsupported token claims version "${version}". Expected "${expectedVersion}".`);
|
|
104
|
+
}
|
|
105
|
+
const issuedAtUnix = readRequiredPositiveSafeIntegerClaim(claims, 'iat');
|
|
106
|
+
const expiresAtUnix = readRequiredPositiveSafeIntegerClaim(claims, 'exp');
|
|
107
|
+
if (expiresAtUnix <= issuedAtUnix) {
|
|
108
|
+
throw new Error('Invalid token lifetime: "exp" must be greater than "iat".');
|
|
109
|
+
}
|
|
110
|
+
const extClaims = claims['ext'];
|
|
111
|
+
if (extClaims !== undefined &&
|
|
112
|
+
(!extClaims || typeof extClaims !== 'object' || Array.isArray(extClaims))) {
|
|
113
|
+
throw new Error('Invalid "ext" claim.');
|
|
114
|
+
}
|
|
115
|
+
const clientId = readOptionalStringClaim(claims, 'cid');
|
|
116
|
+
return {
|
|
117
|
+
sub: readRequiredStringClaim(claims, 'sub'),
|
|
118
|
+
uid: readRequiredStringClaim(claims, 'uid'),
|
|
119
|
+
actor: readRequiredStringClaim(claims, 'actor'),
|
|
120
|
+
...(clientId === undefined ? {} : { cid: clientId }),
|
|
121
|
+
ver: version,
|
|
122
|
+
jti: readRequiredStringClaim(claims, 'jti'),
|
|
123
|
+
...(extClaims === undefined ? {} : { ext: extClaims }),
|
|
124
|
+
iat: issuedAtUnix,
|
|
125
|
+
exp: expiresAtUnix,
|
|
126
|
+
aud: readRequiredStringClaim(claims, 'aud'),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IdCodec } from '../ids/id-codec';
|
|
2
|
+
export type SubjectClaims = {
|
|
3
|
+
sub: string;
|
|
4
|
+
uid: string;
|
|
5
|
+
};
|
|
6
|
+
export type AuthContext<TSessionId, TUserId> = {
|
|
7
|
+
sessionId: TSessionId;
|
|
8
|
+
userId: TUserId;
|
|
9
|
+
};
|
|
10
|
+
export declare function encodeSubjectClaims<TSessionId, TUserId>(input: {
|
|
11
|
+
sessionId: TSessionId;
|
|
12
|
+
userId: TUserId;
|
|
13
|
+
sessionIdCodec: IdCodec<TSessionId>;
|
|
14
|
+
userIdCodec: IdCodec<TUserId>;
|
|
15
|
+
}): SubjectClaims;
|
|
16
|
+
export declare function decodeSubjectClaims<TSessionId, TUserId>(input: {
|
|
17
|
+
claims: unknown;
|
|
18
|
+
sessionIdCodec: IdCodec<TSessionId>;
|
|
19
|
+
userIdCodec: IdCodec<TUserId>;
|
|
20
|
+
}): AuthContext<TSessionId, TUserId>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
function readRequiredStringClaim(rawClaims, claimName) {
|
|
2
|
+
const value = rawClaims[claimName];
|
|
3
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
4
|
+
throw new Error(`Invalid or missing "${claimName}" claim.`);
|
|
5
|
+
}
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
export function encodeSubjectClaims(input) {
|
|
9
|
+
return {
|
|
10
|
+
sub: input.sessionIdCodec.toClaim(input.sessionId),
|
|
11
|
+
uid: input.userIdCodec.toClaim(input.userId),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function decodeSubjectClaims(input) {
|
|
15
|
+
if (!input.claims || typeof input.claims !== 'object') {
|
|
16
|
+
throw new Error('Token claims payload must be an object.');
|
|
17
|
+
}
|
|
18
|
+
const rawClaims = input.claims;
|
|
19
|
+
const subClaim = readRequiredStringClaim(rawClaims, 'sub');
|
|
20
|
+
const uidClaim = readRequiredStringClaim(rawClaims, 'uid');
|
|
21
|
+
return {
|
|
22
|
+
sessionId: input.sessionIdCodec.fromClaim(subClaim),
|
|
23
|
+
userId: input.userIdCodec.fromClaim(uidClaim),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type UserActor = 'USER';
|
|
2
|
+
export type AuthActor = UserActor | string;
|
|
3
|
+
export type SessionRecord<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>> = {
|
|
4
|
+
sessionId: TSessionId;
|
|
5
|
+
userId: TUserId;
|
|
6
|
+
actor: TActor;
|
|
7
|
+
clientId?: string;
|
|
8
|
+
issuedAt: number;
|
|
9
|
+
expiresAt: number;
|
|
10
|
+
revokedAt?: number | null;
|
|
11
|
+
serverSessionData?: TServerSessionData;
|
|
12
|
+
};
|
|
13
|
+
export type AccessClaims<TActor extends AuthActor = AuthActor, TExtClaims extends Record<string, unknown> = Record<string, never>> = {
|
|
14
|
+
sub: string;
|
|
15
|
+
uid: string;
|
|
16
|
+
actor: TActor;
|
|
17
|
+
cid?: string;
|
|
18
|
+
ver: number;
|
|
19
|
+
jti: string;
|
|
20
|
+
ext?: TExtClaims;
|
|
21
|
+
iat: number;
|
|
22
|
+
exp: number;
|
|
23
|
+
aud: string;
|
|
24
|
+
};
|
|
25
|
+
export type ClientSession<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TClientSessionData extends Record<string, unknown> = Record<string, never>> = {
|
|
26
|
+
sessionId: TSessionId;
|
|
27
|
+
userId: TUserId;
|
|
28
|
+
actor: TActor;
|
|
29
|
+
clientId?: string;
|
|
30
|
+
issuedAt: number;
|
|
31
|
+
expiresAt: number;
|
|
32
|
+
clientSessionData?: TClientSessionData;
|
|
33
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withExpressProtectedRoute, type ExpressProtectedAuth, type ExpressProtectedRouteContext, type ExpressRequestLike, type ExpressResponseLike, type WithExpressProtectedRouteOptions, } from './protected-route';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { withExpressProtectedRoute, } from './protected-route';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AccessTokenSource } from '../core/adapters/access-token-transport-adapter';
|
|
2
|
+
import type { ValidatedAccessTokenResult } from '../core/services/contracts';
|
|
3
|
+
import type { DirectAuthService } from '../core/services/direct-auth-service';
|
|
4
|
+
import type { AuthActor } from '../core/types/auth-contract';
|
|
5
|
+
import { type DirectAccessTokenTransportConfig } from '../next/shared/direct-auth-utils';
|
|
6
|
+
type ExpressHeaderValue = string | string[] | undefined;
|
|
7
|
+
export type ExpressRequestLike = {
|
|
8
|
+
headers: Record<string, ExpressHeaderValue>;
|
|
9
|
+
cookies?: Record<string, string | undefined>;
|
|
10
|
+
};
|
|
11
|
+
export type ExpressResponseLike = {
|
|
12
|
+
headersSent?: boolean;
|
|
13
|
+
writableEnded?: boolean;
|
|
14
|
+
setHeader(name: string, value: string): void;
|
|
15
|
+
status(code: number): ExpressResponseLike;
|
|
16
|
+
json(value: unknown): ExpressResponseLike;
|
|
17
|
+
end?: () => void;
|
|
18
|
+
};
|
|
19
|
+
export type ExpressProtectedAuth<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>> = ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims> & {
|
|
20
|
+
accessToken: string;
|
|
21
|
+
accessTokenSource: AccessTokenSource | undefined;
|
|
22
|
+
};
|
|
23
|
+
export type ExpressProtectedRouteContext<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>, TReq extends ExpressRequestLike = ExpressRequestLike, TRes extends ExpressResponseLike = ExpressResponseLike> = {
|
|
24
|
+
req: TReq;
|
|
25
|
+
res: TRes;
|
|
26
|
+
requestId: string | undefined;
|
|
27
|
+
auth: ExpressProtectedAuth<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
|
|
28
|
+
};
|
|
29
|
+
export type WithExpressProtectedRouteOptions<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>, TReq extends ExpressRequestLike = ExpressRequestLike, TRes extends ExpressResponseLike = ExpressResponseLike, TOutput = unknown> = {
|
|
30
|
+
directAuthService: DirectAuthService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
|
|
31
|
+
handler: (input: ExpressProtectedRouteContext<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims, TReq, TRes>) => Promise<TOutput | void> | TOutput | void;
|
|
32
|
+
expectedAudience?: string;
|
|
33
|
+
allowedActors?: readonly TActor[] | ReadonlySet<TActor>;
|
|
34
|
+
accessTokenTransport?: DirectAccessTokenTransportConfig<TActor>;
|
|
35
|
+
requestIdHeaderName?: string;
|
|
36
|
+
authorizationHeaderName?: string;
|
|
37
|
+
challenge?: {
|
|
38
|
+
includeErrorInWwwAuthenticate?: boolean;
|
|
39
|
+
scope?: string;
|
|
40
|
+
resourceMetadataUrl?: string;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export declare function withExpressProtectedRoute<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>, TExtClaims extends Record<string, unknown> = Record<string, never>, TReq extends ExpressRequestLike = ExpressRequestLike, TRes extends ExpressResponseLike = ExpressResponseLike, TOutput = unknown>(options: WithExpressProtectedRouteOptions<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims, TReq, TRes, TOutput>): (req: TReq, res: TRes) => Promise<void>;
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { AuthServiceError, isAuthServiceError, } from '../core/services/auth-error';
|
|
2
|
+
import { parseCookieHeader } from '../next/pages/request';
|
|
3
|
+
import { buildAuthErrorHttpResponse, buildSystemErrorHttpResponse, } from '../next/shared/auth-http';
|
|
4
|
+
import { assertBearerOnlyActorPolicy, getHeaderValue, resolveAccessTokenTransportAdapter, } from '../next/shared/direct-auth-utils';
|
|
5
|
+
function normalizeRequestId(value) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
function buildAccessTokenTransportInput(req, authorizationHeaderName) {
|
|
9
|
+
const authorizationHeader = getHeaderValue(req.headers, authorizationHeaderName);
|
|
10
|
+
const cookies = req.cookies ?? parseCookieHeader(getHeaderValue(req.headers, 'cookie'));
|
|
11
|
+
return {
|
|
12
|
+
...(authorizationHeader === undefined ? {} : { authorizationHeader }),
|
|
13
|
+
...(Object.keys(cookies).length === 0 ? {} : { cookies }),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function isResponseCommitted(res) {
|
|
17
|
+
return res.headersSent === true || res.writableEnded === true;
|
|
18
|
+
}
|
|
19
|
+
function sendExpressAuthError(res, error, input) {
|
|
20
|
+
const built = buildAuthErrorHttpResponse({
|
|
21
|
+
error,
|
|
22
|
+
...(input.requestId === undefined ? {} : { requestId: input.requestId }),
|
|
23
|
+
...(input.challenge?.scope === undefined
|
|
24
|
+
? {}
|
|
25
|
+
: { challengeScope: input.challenge.scope }),
|
|
26
|
+
...(input.challenge?.resourceMetadataUrl === undefined
|
|
27
|
+
? {}
|
|
28
|
+
: { resourceMetadataUrl: input.challenge.resourceMetadataUrl }),
|
|
29
|
+
...(input.challenge?.includeErrorInWwwAuthenticate === undefined
|
|
30
|
+
? {}
|
|
31
|
+
: {
|
|
32
|
+
includeChallengeError: input.challenge.includeErrorInWwwAuthenticate,
|
|
33
|
+
}),
|
|
34
|
+
});
|
|
35
|
+
if (built.challengeHeader !== undefined) {
|
|
36
|
+
res.setHeader('WWW-Authenticate', built.challengeHeader);
|
|
37
|
+
}
|
|
38
|
+
res.status(built.statusCode).json(built.body);
|
|
39
|
+
}
|
|
40
|
+
export function withExpressProtectedRoute(options) {
|
|
41
|
+
const authorizationHeaderName = options.authorizationHeaderName ?? 'authorization';
|
|
42
|
+
const requestIdHeaderName = options.requestIdHeaderName ?? 'x-request-id';
|
|
43
|
+
const accessTokenTransportAdapter = resolveAccessTokenTransportAdapter(options.accessTokenTransport);
|
|
44
|
+
return async function expressProtectedRoute(req, res) {
|
|
45
|
+
const requestId = normalizeRequestId(getHeaderValue(req.headers, requestIdHeaderName));
|
|
46
|
+
const transport = buildAccessTokenTransportInput(req, authorizationHeaderName);
|
|
47
|
+
try {
|
|
48
|
+
const extractedAccessToken = accessTokenTransportAdapter.extractAccessToken({
|
|
49
|
+
transport,
|
|
50
|
+
});
|
|
51
|
+
if (!extractedAccessToken.token) {
|
|
52
|
+
throw new AuthServiceError({
|
|
53
|
+
code: 'invalid_token',
|
|
54
|
+
message: 'Access token is required.',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const validatedAccessToken = await options.directAuthService.validateAccessToken({
|
|
58
|
+
accessToken: extractedAccessToken.token,
|
|
59
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
60
|
+
...(options.expectedAudience === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { expectedAudience: options.expectedAudience }),
|
|
63
|
+
...(options.allowedActors === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: { allowedActors: options.allowedActors }),
|
|
66
|
+
});
|
|
67
|
+
assertBearerOnlyActorPolicy({
|
|
68
|
+
actor: validatedAccessToken.claims.actor,
|
|
69
|
+
...(extractedAccessToken.source === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: { source: extractedAccessToken.source }),
|
|
72
|
+
...(options.accessTokenTransport?.requireBearerForActors === undefined
|
|
73
|
+
? {}
|
|
74
|
+
: {
|
|
75
|
+
requireBearerForActors: options.accessTokenTransport.requireBearerForActors,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
const output = await options.handler({
|
|
79
|
+
req,
|
|
80
|
+
res,
|
|
81
|
+
requestId,
|
|
82
|
+
auth: {
|
|
83
|
+
...validatedAccessToken,
|
|
84
|
+
accessToken: extractedAccessToken.token,
|
|
85
|
+
accessTokenSource: extractedAccessToken.source,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
if (isResponseCommitted(res)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (output === undefined) {
|
|
92
|
+
res.status(204);
|
|
93
|
+
if (typeof res.end === 'function') {
|
|
94
|
+
res.end();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
res.json(null);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
res.status(200).json(output);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (isResponseCommitted(res)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (isAuthServiceError(error)) {
|
|
107
|
+
sendExpressAuthError(res, error, {
|
|
108
|
+
requestId,
|
|
109
|
+
challenge: options.challenge,
|
|
110
|
+
});
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const systemErrorResponse = buildSystemErrorHttpResponse({
|
|
114
|
+
...(requestId === undefined ? {} : { requestId }),
|
|
115
|
+
});
|
|
116
|
+
res.status(systemErrorResponse.statusCode).json(systemErrorResponse.body);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './core/index';
|
|
2
|
+
export * from './config/index';
|
|
3
|
+
export * from './express/index';
|
|
4
|
+
export * as config from './config/index';
|
|
5
|
+
export * as express from './express/index';
|
|
6
|
+
export * as mcp from './mcp/index';
|
|
7
|
+
export * as next from './next/index';
|
|
8
|
+
export * as testing from './testing/index';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './core/index';
|
|
2
|
+
export * from './config/index';
|
|
3
|
+
export * from './express/index';
|
|
4
|
+
export * as config from './config/index';
|
|
5
|
+
export * as express from './express/index';
|
|
6
|
+
export * as mcp from './mcp/index';
|
|
7
|
+
export * as next from './next/index';
|
|
8
|
+
export * as testing from './testing/index';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DEFAULT_UNAUTHENTICATED_RPC_METHODS, getRpcMethodFromBody, getRpcMethodsFromBody, requiresAuth, requiresAuthForBody, } from './json-rpc-auth';
|
package/src/mcp/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DEFAULT_UNAUTHENTICATED_RPC_METHODS, getRpcMethodFromBody, getRpcMethodsFromBody, requiresAuth, requiresAuthForBody, } from './json-rpc-auth';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare const DEFAULT_UNAUTHENTICATED_RPC_METHODS: readonly ["initialize", "ping", "tools/list", "resources/list", "resources/read", "prompts/list", "prompts/get"];
|
|
2
|
+
export declare function getRpcMethodsFromBody(body: unknown): string[];
|
|
3
|
+
export declare function getRpcMethodFromBody(body: unknown): string | null;
|
|
4
|
+
export declare function requiresAuth(rpcMethod: string | null, unauthenticatedMethods?: ReadonlySet<string>): boolean;
|
|
5
|
+
export declare function requiresAuthForBody(body: unknown, unauthenticatedMethods?: ReadonlySet<string>): boolean;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const DEFAULT_UNAUTHENTICATED_RPC_METHODS = Object.freeze([
|
|
2
|
+
'initialize',
|
|
3
|
+
'ping',
|
|
4
|
+
'tools/list',
|
|
5
|
+
'resources/list',
|
|
6
|
+
'resources/read',
|
|
7
|
+
'prompts/list',
|
|
8
|
+
'prompts/get',
|
|
9
|
+
]);
|
|
10
|
+
const DEFAULT_UNAUTHENTICATED_RPC_METHOD_SET = new Set(DEFAULT_UNAUTHENTICATED_RPC_METHODS);
|
|
11
|
+
function extractRpcMethod(payload) {
|
|
12
|
+
if (!payload || typeof payload !== 'object') {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const method = payload.method;
|
|
16
|
+
return typeof method === 'string' ? method : null;
|
|
17
|
+
}
|
|
18
|
+
export function getRpcMethodsFromBody(body) {
|
|
19
|
+
if (Array.isArray(body)) {
|
|
20
|
+
return body
|
|
21
|
+
.map(message => extractRpcMethod(message))
|
|
22
|
+
.filter((method) => method !== null);
|
|
23
|
+
}
|
|
24
|
+
const method = extractRpcMethod(body);
|
|
25
|
+
return method ? [method] : [];
|
|
26
|
+
}
|
|
27
|
+
export function getRpcMethodFromBody(body) {
|
|
28
|
+
return getRpcMethodsFromBody(body)[0] ?? null;
|
|
29
|
+
}
|
|
30
|
+
export function requiresAuth(rpcMethod, unauthenticatedMethods = DEFAULT_UNAUTHENTICATED_RPC_METHOD_SET) {
|
|
31
|
+
if (!rpcMethod) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (rpcMethod.startsWith('notifications/')) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return !unauthenticatedMethods.has(rpcMethod);
|
|
38
|
+
}
|
|
39
|
+
export function requiresAuthForBody(body, unauthenticatedMethods = DEFAULT_UNAUTHENTICATED_RPC_METHOD_SET) {
|
|
40
|
+
return getRpcMethodsFromBody(body).some(method => requiresAuth(method, unauthenticatedMethods));
|
|
41
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type AuthRouteAction, type AuthRouteHttpMethod } from '../shared/auth-routes';
|
|
2
|
+
export type AppAuthCatchAllParams = {
|
|
3
|
+
auth?: string[] | undefined;
|
|
4
|
+
};
|
|
5
|
+
export type AppAuthCatchAllContext = {
|
|
6
|
+
params?: AppAuthCatchAllParams | Promise<AppAuthCatchAllParams>;
|
|
7
|
+
};
|
|
8
|
+
export type AppAuthCatchAllActionHandler = (input: {
|
|
9
|
+
req: Request;
|
|
10
|
+
params: AppAuthCatchAllParams;
|
|
11
|
+
action: AuthRouteAction;
|
|
12
|
+
segments: readonly string[];
|
|
13
|
+
}) => Promise<Response> | Response;
|
|
14
|
+
export type CreateAppAuthCatchAllHandlersOptions = {
|
|
15
|
+
handlers: Partial<Record<AuthRouteAction, AppAuthCatchAllActionHandler>>;
|
|
16
|
+
onUnsupportedRoute?: (input: {
|
|
17
|
+
req: Request;
|
|
18
|
+
params: AppAuthCatchAllParams;
|
|
19
|
+
segments: readonly string[];
|
|
20
|
+
action?: AuthRouteAction;
|
|
21
|
+
}) => Promise<Response> | Response;
|
|
22
|
+
onMethodNotAllowed?: (input: {
|
|
23
|
+
req: Request;
|
|
24
|
+
params: AppAuthCatchAllParams;
|
|
25
|
+
action: AuthRouteAction;
|
|
26
|
+
allowedMethods: readonly AuthRouteHttpMethod[];
|
|
27
|
+
}) => Promise<Response> | Response;
|
|
28
|
+
};
|
|
29
|
+
export type AppAuthCatchAllMethodHandler = (req: Request, context: AppAuthCatchAllContext) => Promise<Response>;
|
|
30
|
+
export type AppAuthCatchAllHandlers = Record<AuthRouteHttpMethod, AppAuthCatchAllMethodHandler>;
|
|
31
|
+
export declare function createAppAuthCatchAllHandlers(options: CreateAppAuthCatchAllHandlersOptions): AppAuthCatchAllHandlers;
|
|
32
|
+
export declare const APP_AUTH_CATCH_ALL_METHODS: readonly AuthRouteHttpMethod[];
|