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,18 @@
|
|
|
1
|
+
export interface PendingAuthRequestAdapter<TPendingAuthRequest> {
|
|
2
|
+
create(input: {
|
|
3
|
+
requestId: string;
|
|
4
|
+
payload: TPendingAuthRequest;
|
|
5
|
+
expiresAtUnix: number;
|
|
6
|
+
}): Promise<void>;
|
|
7
|
+
find(requestId: string): Promise<{
|
|
8
|
+
payload: TPendingAuthRequest;
|
|
9
|
+
expiresAtUnix: number;
|
|
10
|
+
consumedAtUnix?: number | null;
|
|
11
|
+
} | null>;
|
|
12
|
+
consume(requestId: string, nowUnix: number): Promise<{
|
|
13
|
+
payload: TPendingAuthRequest;
|
|
14
|
+
expiresAtUnix: number;
|
|
15
|
+
consumedAtUnix?: number | null;
|
|
16
|
+
} | null>;
|
|
17
|
+
delete(requestId: string): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface RefreshTokenAdapter<TSessionId> {
|
|
2
|
+
create(input: {
|
|
3
|
+
sessionId: TSessionId;
|
|
4
|
+
tokenHash: string;
|
|
5
|
+
expiresAtUnix: number;
|
|
6
|
+
familyId: string;
|
|
7
|
+
parentTokenHash?: string;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
find(tokenHash: string): Promise<{
|
|
10
|
+
sessionId: TSessionId;
|
|
11
|
+
familyId: string;
|
|
12
|
+
expiresAtUnix: number;
|
|
13
|
+
revokedAtUnix?: number | null;
|
|
14
|
+
} | null>;
|
|
15
|
+
consume(tokenHash: string, nowUnix: number): Promise<{
|
|
16
|
+
sessionId: TSessionId;
|
|
17
|
+
familyId: string;
|
|
18
|
+
expiresAtUnix: number;
|
|
19
|
+
consumedAtUnix?: number | null;
|
|
20
|
+
revokedAtUnix?: number | null;
|
|
21
|
+
} | null>;
|
|
22
|
+
revoke(tokenHash: string, nowUnix: number): Promise<void>;
|
|
23
|
+
revokeFamily(familyId: string, nowUnix: number): Promise<void>;
|
|
24
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthActor, SessionRecord } from '../types/auth-contract';
|
|
2
|
+
export interface SessionAdapter<TSessionId, TUserId, TActor extends AuthActor, TServerSessionData extends Record<string, unknown> = Record<string, never>> {
|
|
3
|
+
create(input: {
|
|
4
|
+
userId: TUserId;
|
|
5
|
+
actor: TActor;
|
|
6
|
+
clientId?: string;
|
|
7
|
+
userAgent?: string;
|
|
8
|
+
ipAddress?: string;
|
|
9
|
+
ttlSeconds: number;
|
|
10
|
+
serverSessionData?: TServerSessionData;
|
|
11
|
+
}): Promise<SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>>;
|
|
12
|
+
findById(sessionId: TSessionId): Promise<SessionRecord<TSessionId, TUserId, TActor, TServerSessionData> | null>;
|
|
13
|
+
revoke(sessionId: TSessionId, nowUnix: number): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AccessClaims, AuthActor } from '../types/auth-contract';
|
|
2
|
+
export interface TokenAdapter<TActor extends AuthActor = AuthActor, TExtClaims extends Record<string, unknown> = Record<string, never>> {
|
|
3
|
+
signAccessToken(input: {
|
|
4
|
+
sub: string;
|
|
5
|
+
uid: string;
|
|
6
|
+
actor: TActor;
|
|
7
|
+
cid?: string;
|
|
8
|
+
ver: number;
|
|
9
|
+
jti: string;
|
|
10
|
+
audience: string;
|
|
11
|
+
expiresInSeconds: number;
|
|
12
|
+
ext?: TExtClaims;
|
|
13
|
+
}): string;
|
|
14
|
+
verifyAccessToken(token: string): AccessClaims<TActor, TExtClaims>;
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
function escapeQuotedAttribute(value) {
|
|
2
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
3
|
+
}
|
|
4
|
+
function appendQuotedAttribute(attributes, key, value) {
|
|
5
|
+
if (!value) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
attributes.push(`${key}="${escapeQuotedAttribute(value)}"`);
|
|
9
|
+
}
|
|
10
|
+
export function buildBearerChallengeHeader(options = {}) {
|
|
11
|
+
const headerParts = ['Bearer'];
|
|
12
|
+
appendQuotedAttribute(headerParts, 'scope', options.scope);
|
|
13
|
+
appendQuotedAttribute(headerParts, 'resource_metadata', options.resourceMetadataUrl);
|
|
14
|
+
appendQuotedAttribute(headerParts, 'error', options.error);
|
|
15
|
+
return headerParts.join(', ');
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
function isCanonicalPositiveIntegerClaim(value) {
|
|
2
|
+
return /^[1-9]\d*$/u.test(value);
|
|
3
|
+
}
|
|
4
|
+
function assertSafePositiveInteger(value, operation) {
|
|
5
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
6
|
+
throw new Error(`${operation} expects a positive safe integer.`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export const stringIdCodec = {
|
|
10
|
+
toClaim(value) {
|
|
11
|
+
return value;
|
|
12
|
+
},
|
|
13
|
+
fromClaim(value) {
|
|
14
|
+
return value;
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
export const numberIdCodec = {
|
|
18
|
+
toClaim(value) {
|
|
19
|
+
assertSafePositiveInteger(value, 'numberIdCodec.toClaim');
|
|
20
|
+
return String(value);
|
|
21
|
+
},
|
|
22
|
+
fromClaim(value) {
|
|
23
|
+
if (!isCanonicalPositiveIntegerClaim(value)) {
|
|
24
|
+
throw new Error('numberIdCodec.fromClaim expects a positive integer claim.');
|
|
25
|
+
}
|
|
26
|
+
const parsed = Number(value);
|
|
27
|
+
assertSafePositiveInteger(parsed, 'numberIdCodec.fromClaim');
|
|
28
|
+
return parsed;
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { buildBearerChallengeHeader, type BearerChallengeOptions, } from './http/bearer-challenge';
|
|
2
|
+
export { base64UrlEncode, createS256CodeChallenge, verifyPkce, type PkceCodeMethod, type VerifyPkceParams, } from './oauth/pkce';
|
|
3
|
+
export { numberIdCodec, stringIdCodec, type IdCodec } from './ids/id-codec';
|
|
4
|
+
export { decodeSubjectClaims, encodeSubjectClaims, type AuthContext, type SubjectClaims, } from './tokens/id-claims';
|
|
5
|
+
export * from './services/index';
|
|
6
|
+
export { DEFAULT_TOKEN_CLAIMS_VERSION, buildAccessClaims, parseAccessClaims, type BuildAccessClaimsInput, } from './tokens/access-claims';
|
|
7
|
+
export { buildClientSession } from './sessions/client-session';
|
|
8
|
+
export type { AccessClaims, AuthActor, ClientSession, SessionRecord, UserActor, } from './types/auth-contract';
|
|
9
|
+
export type * from './adapters/index';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { buildBearerChallengeHeader, } from './http/bearer-challenge';
|
|
2
|
+
export { base64UrlEncode, createS256CodeChallenge, verifyPkce, } from './oauth/pkce';
|
|
3
|
+
export { numberIdCodec, stringIdCodec } from './ids/id-codec';
|
|
4
|
+
export { decodeSubjectClaims, encodeSubjectClaims, } from './tokens/id-claims';
|
|
5
|
+
export * from './services/index';
|
|
6
|
+
export { DEFAULT_TOKEN_CLAIMS_VERSION, buildAccessClaims, parseAccessClaims, } from './tokens/access-claims';
|
|
7
|
+
export { buildClientSession } from './sessions/client-session';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type PkceCodeMethod = 'plain' | 'S256';
|
|
2
|
+
export type VerifyPkceParams = {
|
|
3
|
+
codeVerifier: string;
|
|
4
|
+
codeChallenge: string;
|
|
5
|
+
codeMethod: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function base64UrlEncode(value: Uint8Array): string;
|
|
8
|
+
export declare function createS256CodeChallenge(codeVerifier: string): string;
|
|
9
|
+
export declare function verifyPkce(params: VerifyPkceParams): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
export function base64UrlEncode(value) {
|
|
3
|
+
return Buffer.from(value)
|
|
4
|
+
.toString('base64')
|
|
5
|
+
.replace(/\+/g, '-')
|
|
6
|
+
.replace(/\//g, '_')
|
|
7
|
+
.replace(/=+$/u, '');
|
|
8
|
+
}
|
|
9
|
+
export function createS256CodeChallenge(codeVerifier) {
|
|
10
|
+
const digest = createHash('sha256').update(codeVerifier, 'utf8').digest();
|
|
11
|
+
return base64UrlEncode(digest);
|
|
12
|
+
}
|
|
13
|
+
function constantTimeEqual(left, right) {
|
|
14
|
+
const leftBuffer = Buffer.from(left, 'utf8');
|
|
15
|
+
const rightBuffer = Buffer.from(right, 'utf8');
|
|
16
|
+
if (leftBuffer.length !== rightBuffer.length) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return timingSafeEqual(leftBuffer, rightBuffer);
|
|
20
|
+
}
|
|
21
|
+
export function verifyPkce(params) {
|
|
22
|
+
if (params.codeMethod === 'plain') {
|
|
23
|
+
return constantTimeEqual(params.codeVerifier, params.codeChallenge);
|
|
24
|
+
}
|
|
25
|
+
if (params.codeMethod === 'S256') {
|
|
26
|
+
const computedChallenge = createS256CodeChallenge(params.codeVerifier);
|
|
27
|
+
return constantTimeEqual(computedChallenge, params.codeChallenge);
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AccessTokenRevocationAdapter } from '../adapters/access-token-revocation-adapter';
|
|
2
|
+
import type { ObservabilityConfig } from '../adapters/observability-hooks';
|
|
3
|
+
import type { SessionAdapter } from '../adapters/session-adapter';
|
|
4
|
+
import type { TokenAdapter } from '../adapters/token-adapter';
|
|
5
|
+
import type { IdCodec } from '../ids/id-codec';
|
|
6
|
+
import type { AuthActor, SessionRecord } from '../types/auth-contract';
|
|
7
|
+
import type { IssuedAccessToken, ValidatedAccessTokenResult } from './contracts';
|
|
8
|
+
import { type RevocationGuarantee, type SessionMode } from './revocation-policy';
|
|
9
|
+
export type AccessTokenService<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>> = {
|
|
10
|
+
issueAccessToken(input: {
|
|
11
|
+
session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
12
|
+
requestId?: string;
|
|
13
|
+
}): Promise<IssuedAccessToken<TSessionId, TUserId, TActor, TExtClaims, TClientSessionData>>;
|
|
14
|
+
validateAccessToken(input: {
|
|
15
|
+
accessToken: string;
|
|
16
|
+
requestId?: string;
|
|
17
|
+
expectedAudience?: string;
|
|
18
|
+
allowedActors?: readonly TActor[] | ReadonlySet<TActor>;
|
|
19
|
+
}): Promise<ValidatedAccessTokenResult<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>>;
|
|
20
|
+
};
|
|
21
|
+
export type CreateAccessTokenServiceInput<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>> = {
|
|
22
|
+
tokenAdapter: TokenAdapter<TActor, TExtClaims>;
|
|
23
|
+
sessionIdCodec: IdCodec<TSessionId>;
|
|
24
|
+
userIdCodec: IdCodec<TUserId>;
|
|
25
|
+
sessionAdapter?: SessionAdapter<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
26
|
+
accessTokenRevocationAdapter?: AccessTokenRevocationAdapter;
|
|
27
|
+
sessionMode?: SessionMode;
|
|
28
|
+
revocationGuarantee?: RevocationGuarantee;
|
|
29
|
+
boundedRevocationCacheSeconds?: number;
|
|
30
|
+
audience: string;
|
|
31
|
+
accessTokenTtlSeconds: number;
|
|
32
|
+
tokenClaimsVersion?: number;
|
|
33
|
+
nowUnix?: () => number;
|
|
34
|
+
buildExtClaims?: (input: {
|
|
35
|
+
session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
36
|
+
}) => TExtClaims;
|
|
37
|
+
buildClientSessionData?: (input: {
|
|
38
|
+
session: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
39
|
+
}) => TClientSessionData;
|
|
40
|
+
observability?: ObservabilityConfig<TActor>;
|
|
41
|
+
};
|
|
42
|
+
export declare function createAccessTokenService<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>>(input: CreateAccessTokenServiceInput<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>): AccessTokenService<TSessionId, TUserId, TActor, TServerSessionData, TClientSessionData, TExtClaims>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { buildClientSession } from '../sessions/client-session';
|
|
2
|
+
import { decodeSubjectClaims } from '../tokens/id-claims';
|
|
3
|
+
import { buildAccessClaims, parseAccessClaims } from '../tokens/access-claims';
|
|
4
|
+
import { AuthServiceError, isAuthServiceError } from './auth-error';
|
|
5
|
+
import { createRevocationDecisionEngine, shouldValidateSession, } from './revocation-policy';
|
|
6
|
+
import { emitAuditEvent, emitMetric, reportServiceError, } from './observability';
|
|
7
|
+
function assertPositiveSafeInteger(value, fieldName) {
|
|
8
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
9
|
+
throw new AuthServiceError({
|
|
10
|
+
code: 'invalid_request',
|
|
11
|
+
message: `${fieldName} must be a positive safe integer.`,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function normalizeAllowedActors(actors) {
|
|
16
|
+
if (actors === undefined) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (actors instanceof Set) {
|
|
20
|
+
return actors;
|
|
21
|
+
}
|
|
22
|
+
return new Set(actors);
|
|
23
|
+
}
|
|
24
|
+
export function createAccessTokenService(input) {
|
|
25
|
+
if (input.audience.length === 0) {
|
|
26
|
+
throw new AuthServiceError({
|
|
27
|
+
code: 'invalid_request',
|
|
28
|
+
message: 'audience must be a non-empty string.',
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
assertPositiveSafeInteger(input.accessTokenTtlSeconds, 'accessTokenTtlSeconds');
|
|
32
|
+
const sessionMode = input.sessionMode ?? 'hybrid';
|
|
33
|
+
const revocationGuarantee = input.revocationGuarantee ?? 'strict';
|
|
34
|
+
const boundedRevocationCacheSeconds = input.boundedRevocationCacheSeconds ?? 30;
|
|
35
|
+
if (sessionMode === 'stateful' && !input.sessionAdapter) {
|
|
36
|
+
throw new AuthServiceError({
|
|
37
|
+
code: 'invalid_request',
|
|
38
|
+
message: 'sessionAdapter is required when sessionMode is stateful.',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (revocationGuarantee === 'bounded') {
|
|
42
|
+
assertPositiveSafeInteger(boundedRevocationCacheSeconds, 'boundedRevocationCacheSeconds');
|
|
43
|
+
}
|
|
44
|
+
const nowUnix = input.nowUnix ?? (() => Math.floor(Date.now() / 1000));
|
|
45
|
+
const revocationDecisionEngine = createRevocationDecisionEngine();
|
|
46
|
+
return {
|
|
47
|
+
async issueAccessToken({ session, requestId }) {
|
|
48
|
+
const issuedAtUnix = nowUnix();
|
|
49
|
+
try {
|
|
50
|
+
const claims = buildAccessClaims({
|
|
51
|
+
session,
|
|
52
|
+
sessionIdCodec: input.sessionIdCodec,
|
|
53
|
+
userIdCodec: input.userIdCodec,
|
|
54
|
+
audience: input.audience,
|
|
55
|
+
accessTokenTtlSeconds: input.accessTokenTtlSeconds,
|
|
56
|
+
...(input.tokenClaimsVersion === undefined
|
|
57
|
+
? {}
|
|
58
|
+
: { tokenClaimsVersion: input.tokenClaimsVersion }),
|
|
59
|
+
nowUnix: issuedAtUnix,
|
|
60
|
+
...(input.buildExtClaims === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { buildExtClaims: input.buildExtClaims }),
|
|
63
|
+
});
|
|
64
|
+
const accessToken = input.tokenAdapter.signAccessToken({
|
|
65
|
+
sub: claims.sub,
|
|
66
|
+
uid: claims.uid,
|
|
67
|
+
actor: claims.actor,
|
|
68
|
+
...(claims.cid === undefined ? {} : { cid: claims.cid }),
|
|
69
|
+
ver: claims.ver,
|
|
70
|
+
jti: claims.jti,
|
|
71
|
+
audience: claims.aud,
|
|
72
|
+
expiresInSeconds: claims.exp - claims.iat,
|
|
73
|
+
...(claims.ext === undefined ? {} : { ext: claims.ext }),
|
|
74
|
+
});
|
|
75
|
+
const clientSession = buildClientSession({
|
|
76
|
+
session,
|
|
77
|
+
...(input.buildClientSessionData === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { buildClientSessionData: input.buildClientSessionData }),
|
|
80
|
+
});
|
|
81
|
+
await emitAuditEvent({
|
|
82
|
+
observability: input.observability,
|
|
83
|
+
requestId,
|
|
84
|
+
event: {
|
|
85
|
+
name: 'access_token_issued',
|
|
86
|
+
atUnix: issuedAtUnix,
|
|
87
|
+
actor: claims.actor,
|
|
88
|
+
userId: claims.uid,
|
|
89
|
+
...(claims.cid === undefined ? {} : { clientId: claims.cid }),
|
|
90
|
+
sessionId: claims.sub,
|
|
91
|
+
jti: claims.jti,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
await emitMetric({
|
|
95
|
+
observability: input.observability,
|
|
96
|
+
requestId,
|
|
97
|
+
metric: {
|
|
98
|
+
name: 'auth.access_token.issued',
|
|
99
|
+
value: 1,
|
|
100
|
+
tags: { actor: String(claims.actor) },
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
accessToken,
|
|
105
|
+
claims,
|
|
106
|
+
clientSession,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
if (isAuthServiceError(error)) {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
await reportServiceError({
|
|
114
|
+
observability: input.observability,
|
|
115
|
+
where: 'access-token-service.issueAccessToken',
|
|
116
|
+
error,
|
|
117
|
+
requestId,
|
|
118
|
+
});
|
|
119
|
+
throw new AuthServiceError({
|
|
120
|
+
code: 'system_error',
|
|
121
|
+
message: 'Failed to issue access token.',
|
|
122
|
+
cause: error,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
async validateAccessToken({ accessToken, requestId, expectedAudience, allowedActors, }) {
|
|
127
|
+
const now = nowUnix();
|
|
128
|
+
let parsedClaims;
|
|
129
|
+
const reject = async (error) => {
|
|
130
|
+
await emitAuditEvent({
|
|
131
|
+
observability: input.observability,
|
|
132
|
+
requestId,
|
|
133
|
+
event: {
|
|
134
|
+
name: 'access_token_rejected',
|
|
135
|
+
atUnix: now,
|
|
136
|
+
...(parsedClaims === undefined
|
|
137
|
+
? {}
|
|
138
|
+
: {
|
|
139
|
+
actor: parsedClaims.actor,
|
|
140
|
+
userId: parsedClaims.uid,
|
|
141
|
+
clientId: parsedClaims.cid,
|
|
142
|
+
sessionId: parsedClaims.sub,
|
|
143
|
+
jti: parsedClaims.jti,
|
|
144
|
+
}),
|
|
145
|
+
reason: error.code,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
await emitMetric({
|
|
149
|
+
observability: input.observability,
|
|
150
|
+
requestId,
|
|
151
|
+
metric: {
|
|
152
|
+
name: 'auth.access_token.rejected',
|
|
153
|
+
value: 1,
|
|
154
|
+
tags: { code: error.code },
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
throw error;
|
|
158
|
+
};
|
|
159
|
+
try {
|
|
160
|
+
try {
|
|
161
|
+
parsedClaims = parseAccessClaims({
|
|
162
|
+
claims: input.tokenAdapter.verifyAccessToken(accessToken),
|
|
163
|
+
...(input.tokenClaimsVersion === undefined
|
|
164
|
+
? {}
|
|
165
|
+
: { tokenClaimsVersion: input.tokenClaimsVersion }),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return reject(new AuthServiceError({
|
|
170
|
+
code: 'invalid_token',
|
|
171
|
+
message: 'Access token verification failed.',
|
|
172
|
+
cause: error,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
const audience = expectedAudience ?? input.audience;
|
|
176
|
+
if (parsedClaims.aud !== audience) {
|
|
177
|
+
return reject(new AuthServiceError({
|
|
178
|
+
code: 'invalid_token',
|
|
179
|
+
message: 'Access token audience mismatch.',
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
if (parsedClaims.exp <= now) {
|
|
183
|
+
return reject(new AuthServiceError({
|
|
184
|
+
code: 'token_expired',
|
|
185
|
+
message: 'Access token has expired.',
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
const allowedActorSet = normalizeAllowedActors(allowedActors);
|
|
189
|
+
if (allowedActorSet && !allowedActorSet.has(parsedClaims.actor)) {
|
|
190
|
+
return reject(new AuthServiceError({
|
|
191
|
+
code: 'forbidden',
|
|
192
|
+
message: 'Actor is not allowed for this resource.',
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
if (await revocationDecisionEngine.isAccessTokenRevoked({
|
|
196
|
+
jti: parsedClaims.jti,
|
|
197
|
+
nowUnix: now,
|
|
198
|
+
revocationGuarantee,
|
|
199
|
+
boundedRevocationCacheSeconds,
|
|
200
|
+
...(input.accessTokenRevocationAdapter === undefined
|
|
201
|
+
? {}
|
|
202
|
+
: {
|
|
203
|
+
accessTokenRevocationAdapter: input.accessTokenRevocationAdapter,
|
|
204
|
+
}),
|
|
205
|
+
})) {
|
|
206
|
+
return reject(new AuthServiceError({
|
|
207
|
+
code: 'token_revoked',
|
|
208
|
+
message: 'Access token was revoked.',
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
let authContext;
|
|
212
|
+
try {
|
|
213
|
+
authContext = decodeSubjectClaims({
|
|
214
|
+
claims: parsedClaims,
|
|
215
|
+
sessionIdCodec: input.sessionIdCodec,
|
|
216
|
+
userIdCodec: input.userIdCodec,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
return reject(new AuthServiceError({
|
|
221
|
+
code: 'invalid_token',
|
|
222
|
+
message: 'Access token subject claims are invalid.',
|
|
223
|
+
cause: error,
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
let session;
|
|
227
|
+
let clientSession;
|
|
228
|
+
if (shouldValidateSession({
|
|
229
|
+
sessionMode,
|
|
230
|
+
hasSessionAdapter: input.sessionAdapter !== undefined,
|
|
231
|
+
})) {
|
|
232
|
+
const foundSession = await input.sessionAdapter.findById(authContext.sessionId);
|
|
233
|
+
if (!foundSession) {
|
|
234
|
+
return reject(new AuthServiceError({
|
|
235
|
+
code: 'session_not_found',
|
|
236
|
+
message: 'Session was not found.',
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
session = foundSession;
|
|
240
|
+
if (session.userId !== authContext.userId) {
|
|
241
|
+
return reject(new AuthServiceError({
|
|
242
|
+
code: 'unauthorized',
|
|
243
|
+
message: 'Session user does not match token user.',
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
if (session.actor !== parsedClaims.actor) {
|
|
247
|
+
return reject(new AuthServiceError({
|
|
248
|
+
code: 'unauthorized',
|
|
249
|
+
message: 'Session actor does not match token actor.',
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
if (session.clientId !== parsedClaims.cid) {
|
|
253
|
+
return reject(new AuthServiceError({
|
|
254
|
+
code: 'unauthorized',
|
|
255
|
+
message: 'Session client does not match token client.',
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
if (session.revokedAt !== undefined &&
|
|
259
|
+
session.revokedAt !== null &&
|
|
260
|
+
session.revokedAt <= now) {
|
|
261
|
+
return reject(new AuthServiceError({
|
|
262
|
+
code: 'session_revoked',
|
|
263
|
+
message: 'Session was revoked.',
|
|
264
|
+
}));
|
|
265
|
+
}
|
|
266
|
+
if (session.expiresAt <= now) {
|
|
267
|
+
return reject(new AuthServiceError({
|
|
268
|
+
code: 'session_expired',
|
|
269
|
+
message: 'Session has expired.',
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
clientSession = buildClientSession({
|
|
273
|
+
session,
|
|
274
|
+
...(input.buildClientSessionData === undefined
|
|
275
|
+
? {}
|
|
276
|
+
: { buildClientSessionData: input.buildClientSessionData }),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
claims: parsedClaims,
|
|
281
|
+
authContext,
|
|
282
|
+
...(session === undefined ? {} : { session }),
|
|
283
|
+
...(clientSession === undefined ? {} : { clientSession }),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
if (isAuthServiceError(error)) {
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
await reportServiceError({
|
|
291
|
+
observability: input.observability,
|
|
292
|
+
where: 'access-token-service.validateAccessToken',
|
|
293
|
+
error,
|
|
294
|
+
requestId,
|
|
295
|
+
});
|
|
296
|
+
throw new AuthServiceError({
|
|
297
|
+
code: 'system_error',
|
|
298
|
+
message: 'Failed to validate access token.',
|
|
299
|
+
cause: error,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AuthServiceErrorCode = 'invalid_request' | 'invalid_client' | 'invalid_grant' | 'invalid_token' | 'token_expired' | 'token_revoked' | 'session_not_found' | 'session_revoked' | 'session_expired' | 'unauthorized' | 'forbidden' | 'system_error';
|
|
2
|
+
export declare class AuthServiceError extends Error {
|
|
3
|
+
readonly code: AuthServiceErrorCode;
|
|
4
|
+
readonly details?: Record<string, unknown>;
|
|
5
|
+
readonly cause?: unknown;
|
|
6
|
+
constructor(input: {
|
|
7
|
+
code: AuthServiceErrorCode;
|
|
8
|
+
message: string;
|
|
9
|
+
details?: Record<string, unknown>;
|
|
10
|
+
cause?: unknown;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export declare function isAuthServiceError(error: unknown): error is AuthServiceError;
|
|
14
|
+
export declare function authErrorToHttpStatus(code: AuthServiceErrorCode): number;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export class AuthServiceError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
details;
|
|
4
|
+
cause;
|
|
5
|
+
constructor(input) {
|
|
6
|
+
super(input.message);
|
|
7
|
+
this.name = 'AuthServiceError';
|
|
8
|
+
this.code = input.code;
|
|
9
|
+
if (input.details !== undefined) {
|
|
10
|
+
this.details = input.details;
|
|
11
|
+
}
|
|
12
|
+
if (input.cause !== undefined) {
|
|
13
|
+
this.cause = input.cause;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isAuthServiceError(error) {
|
|
18
|
+
return error instanceof AuthServiceError;
|
|
19
|
+
}
|
|
20
|
+
export function authErrorToHttpStatus(code) {
|
|
21
|
+
switch (code) {
|
|
22
|
+
case 'invalid_request':
|
|
23
|
+
return 400;
|
|
24
|
+
case 'invalid_client':
|
|
25
|
+
return 401;
|
|
26
|
+
case 'invalid_grant':
|
|
27
|
+
return 400;
|
|
28
|
+
case 'invalid_token':
|
|
29
|
+
return 401;
|
|
30
|
+
case 'token_expired':
|
|
31
|
+
return 401;
|
|
32
|
+
case 'token_revoked':
|
|
33
|
+
return 401;
|
|
34
|
+
case 'session_not_found':
|
|
35
|
+
return 401;
|
|
36
|
+
case 'session_revoked':
|
|
37
|
+
return 401;
|
|
38
|
+
case 'session_expired':
|
|
39
|
+
return 401;
|
|
40
|
+
case 'unauthorized':
|
|
41
|
+
return 401;
|
|
42
|
+
case 'forbidden':
|
|
43
|
+
return 403;
|
|
44
|
+
case 'system_error':
|
|
45
|
+
return 500;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { AuthContext } from '../tokens/id-claims';
|
|
2
|
+
import type { AccessClaims, AuthActor, ClientSession, SessionRecord } from '../types/auth-contract';
|
|
3
|
+
export type RefreshTokenMode = 'reuse' | 'rotate' | 'rotate_with_reuse_detection';
|
|
4
|
+
export type RefreshTokenReuseRevokeScope = 'token' | 'family' | 'session';
|
|
5
|
+
export type IssuedAccessToken<TSessionId, TUserId, TActor extends AuthActor = AuthActor, TExtClaims extends Record<string, unknown> = Record<string, never>, TClientSessionData extends Record<string, unknown> = Record<string, never>> = {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
claims: AccessClaims<TActor, TExtClaims>;
|
|
8
|
+
clientSession?: ClientSession<TSessionId, TUserId, TActor, TClientSessionData>;
|
|
9
|
+
};
|
|
10
|
+
export type ValidatedAccessTokenResult<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>> = {
|
|
11
|
+
claims: AccessClaims<TActor, TExtClaims>;
|
|
12
|
+
authContext: AuthContext<TSessionId, TUserId>;
|
|
13
|
+
session?: SessionRecord<TSessionId, TUserId, TActor, TServerSessionData>;
|
|
14
|
+
clientSession?: ClientSession<TSessionId, TUserId, TActor, TClientSessionData>;
|
|
15
|
+
};
|
|
16
|
+
export type OAuthPendingRequest = {
|
|
17
|
+
clientId: string;
|
|
18
|
+
redirectUri: string;
|
|
19
|
+
codeChallenge: string;
|
|
20
|
+
codeMethod: 'S256' | 'plain';
|
|
21
|
+
scopes: string[];
|
|
22
|
+
state?: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|