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,45 @@
|
|
|
1
|
+
export declare function parseBodyToRecord(body: unknown): Record<string, unknown>;
|
|
2
|
+
export declare function parseCodeMethod(value: unknown, defaultMethod?: 'S256' | 'plain'): 'S256' | 'plain';
|
|
3
|
+
export declare function parseScopes(value: unknown, fallback: readonly string[]): string[];
|
|
4
|
+
export declare function parseBoolean(value: unknown, defaultValue: boolean): boolean;
|
|
5
|
+
export declare function parsePositiveSafeInteger(value: unknown, fieldName: string): number;
|
|
6
|
+
export type ParsedAuthorizeRequest = {
|
|
7
|
+
responseType: 'code';
|
|
8
|
+
clientId: string;
|
|
9
|
+
redirectUri: string;
|
|
10
|
+
codeChallenge: string;
|
|
11
|
+
codeMethod: 'S256' | 'plain';
|
|
12
|
+
scopes: string[];
|
|
13
|
+
state?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function parseAuthorizeRequest(input: {
|
|
16
|
+
query: Record<string, unknown>;
|
|
17
|
+
requiredScope: string;
|
|
18
|
+
defaultCodeMethod?: 'S256' | 'plain';
|
|
19
|
+
requireState?: boolean;
|
|
20
|
+
}): ParsedAuthorizeRequest;
|
|
21
|
+
export type ParsedOAuthTokenRequest = {
|
|
22
|
+
grantType: 'authorization_code';
|
|
23
|
+
clientId: string;
|
|
24
|
+
clientSecret: string | null;
|
|
25
|
+
authorizationCode: string;
|
|
26
|
+
redirectUri: string;
|
|
27
|
+
codeVerifier: string;
|
|
28
|
+
} | {
|
|
29
|
+
grantType: 'refresh_token';
|
|
30
|
+
clientId: string;
|
|
31
|
+
clientSecret: string | null;
|
|
32
|
+
refreshToken: string;
|
|
33
|
+
} | {
|
|
34
|
+
grantType: 'client_credentials';
|
|
35
|
+
clientId: string;
|
|
36
|
+
clientSecret: string | null;
|
|
37
|
+
};
|
|
38
|
+
export declare function parseOAuthTokenRequest(input: {
|
|
39
|
+
body: unknown;
|
|
40
|
+
authorizationHeader?: string;
|
|
41
|
+
allowClientCredentials?: boolean;
|
|
42
|
+
allowGrantTypeInference?: boolean;
|
|
43
|
+
}): ParsedOAuthTokenRequest;
|
|
44
|
+
export declare function appendUrlParams(baseUrl: string, params: Record<string, string | undefined>): string;
|
|
45
|
+
export declare function toSingleString(value: unknown): string | undefined;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { AuthServiceError } from '../../core/services/auth-error';
|
|
2
|
+
function toNonEmptyString(value) {
|
|
3
|
+
if (Array.isArray(value)) {
|
|
4
|
+
for (const item of value) {
|
|
5
|
+
const normalized = toNonEmptyString(item);
|
|
6
|
+
if (normalized !== undefined) {
|
|
7
|
+
return normalized;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (typeof value !== 'string') {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const trimmed = value.trim();
|
|
16
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
17
|
+
}
|
|
18
|
+
function assertObjectRecord(value) {
|
|
19
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
20
|
+
throw new AuthServiceError({
|
|
21
|
+
code: 'invalid_request',
|
|
22
|
+
message: 'Request body must be an object.',
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function parseStringBody(body) {
|
|
28
|
+
const trimmed = body.trim();
|
|
29
|
+
if (trimmed.length === 0) {
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
let parsedJson;
|
|
33
|
+
try {
|
|
34
|
+
parsedJson = JSON.parse(trimmed);
|
|
35
|
+
}
|
|
36
|
+
catch (jsonError) {
|
|
37
|
+
const params = new URLSearchParams(trimmed);
|
|
38
|
+
if (params.size === 0) {
|
|
39
|
+
throw new AuthServiceError({
|
|
40
|
+
code: 'invalid_request',
|
|
41
|
+
message: 'Request body must be valid JSON or URL-encoded form data.',
|
|
42
|
+
cause: jsonError,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const output = {};
|
|
46
|
+
params.forEach((paramValue, paramName) => {
|
|
47
|
+
output[paramName] = paramValue;
|
|
48
|
+
});
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
return assertObjectRecord(parsedJson);
|
|
52
|
+
}
|
|
53
|
+
export function parseBodyToRecord(body) {
|
|
54
|
+
if (body === null || body === undefined) {
|
|
55
|
+
return {};
|
|
56
|
+
}
|
|
57
|
+
if (typeof body === 'string') {
|
|
58
|
+
return parseStringBody(body);
|
|
59
|
+
}
|
|
60
|
+
if (body instanceof URLSearchParams) {
|
|
61
|
+
const output = {};
|
|
62
|
+
body.forEach((paramValue, paramName) => {
|
|
63
|
+
output[paramName] = paramValue;
|
|
64
|
+
});
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
return assertObjectRecord(body);
|
|
68
|
+
}
|
|
69
|
+
export function parseCodeMethod(value, defaultMethod = 'S256') {
|
|
70
|
+
const normalized = toNonEmptyString(value);
|
|
71
|
+
if (normalized === undefined) {
|
|
72
|
+
return defaultMethod;
|
|
73
|
+
}
|
|
74
|
+
if (normalized !== 'S256' && normalized !== 'plain') {
|
|
75
|
+
throw new AuthServiceError({
|
|
76
|
+
code: 'invalid_request',
|
|
77
|
+
message: 'Unsupported PKCE code_challenge_method.',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
82
|
+
export function parseScopes(value, fallback) {
|
|
83
|
+
const normalized = toNonEmptyString(value);
|
|
84
|
+
if (normalized === undefined) {
|
|
85
|
+
return [...fallback];
|
|
86
|
+
}
|
|
87
|
+
return normalized
|
|
88
|
+
.split(/\s+/)
|
|
89
|
+
.map(scope => scope.trim())
|
|
90
|
+
.filter(scope => scope.length > 0);
|
|
91
|
+
}
|
|
92
|
+
export function parseBoolean(value, defaultValue) {
|
|
93
|
+
if (value === undefined || value === null) {
|
|
94
|
+
return defaultValue;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === 'boolean') {
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
if (typeof value === 'number') {
|
|
100
|
+
if (value === 1) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (value === 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (typeof value === 'string') {
|
|
108
|
+
const normalized = value.trim().toLowerCase();
|
|
109
|
+
if (normalized === 'true' || normalized === '1' || normalized === 'yes') {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
if (normalized === 'false' || normalized === '0' || normalized === 'no') {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
throw new AuthServiceError({
|
|
117
|
+
code: 'invalid_request',
|
|
118
|
+
message: 'Boolean field is invalid.',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
export function parsePositiveSafeInteger(value, fieldName) {
|
|
122
|
+
const normalized = typeof value === 'number'
|
|
123
|
+
? value
|
|
124
|
+
: Number.parseInt(toNonEmptyString(value) ?? '', 10);
|
|
125
|
+
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
126
|
+
throw new AuthServiceError({
|
|
127
|
+
code: 'invalid_request',
|
|
128
|
+
message: `${fieldName} must be a positive safe integer.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return normalized;
|
|
132
|
+
}
|
|
133
|
+
function parseBasicCredentials(authorizationHeader) {
|
|
134
|
+
if (!authorizationHeader) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const match = authorizationHeader.match(/^Basic\s+(.+)$/i);
|
|
138
|
+
if (!match) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let decoded;
|
|
142
|
+
try {
|
|
143
|
+
decoded = Buffer.from(match[1], 'base64').toString('utf8');
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
throw new AuthServiceError({
|
|
147
|
+
code: 'invalid_request',
|
|
148
|
+
message: 'Invalid Basic authorization header.',
|
|
149
|
+
cause: error,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const separatorIndex = decoded.indexOf(':');
|
|
153
|
+
if (separatorIndex <= 0) {
|
|
154
|
+
throw new AuthServiceError({
|
|
155
|
+
code: 'invalid_request',
|
|
156
|
+
message: 'Invalid Basic authorization header.',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const clientId = decoded.slice(0, separatorIndex);
|
|
160
|
+
const clientSecretRaw = decoded.slice(separatorIndex + 1);
|
|
161
|
+
return {
|
|
162
|
+
clientId,
|
|
163
|
+
clientSecret: clientSecretRaw.length > 0 ? clientSecretRaw : null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function requireField(body, fieldName) {
|
|
167
|
+
const value = toNonEmptyString(body[fieldName]);
|
|
168
|
+
if (value === undefined) {
|
|
169
|
+
throw new AuthServiceError({
|
|
170
|
+
code: 'invalid_request',
|
|
171
|
+
message: `Missing required field "${fieldName}".`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
export function parseAuthorizeRequest(input) {
|
|
177
|
+
const responseTypeRaw = toNonEmptyString(input.query.response_type) ?? 'code';
|
|
178
|
+
if (responseTypeRaw !== 'code') {
|
|
179
|
+
throw new AuthServiceError({
|
|
180
|
+
code: 'invalid_request',
|
|
181
|
+
message: 'Unsupported response_type.',
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const clientId = toNonEmptyString(input.query.client_id);
|
|
185
|
+
const redirectUri = toNonEmptyString(input.query.redirect_uri);
|
|
186
|
+
const codeChallenge = toNonEmptyString(input.query.code_challenge);
|
|
187
|
+
const state = toNonEmptyString(input.query.state);
|
|
188
|
+
if (clientId === undefined || redirectUri === undefined) {
|
|
189
|
+
throw new AuthServiceError({
|
|
190
|
+
code: 'invalid_request',
|
|
191
|
+
message: 'Missing required authorize query parameters.',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (codeChallenge === undefined) {
|
|
195
|
+
throw new AuthServiceError({
|
|
196
|
+
code: 'invalid_request',
|
|
197
|
+
message: 'Missing required query parameter "code_challenge".',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const requireState = input.requireState ?? true;
|
|
201
|
+
if (requireState && state === undefined) {
|
|
202
|
+
throw new AuthServiceError({
|
|
203
|
+
code: 'invalid_request',
|
|
204
|
+
message: 'Missing required query parameter "state".',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const codeMethod = parseCodeMethod(input.query.code_challenge_method, input.defaultCodeMethod ?? 'S256');
|
|
208
|
+
const scopes = parseScopes(input.query.scope, [input.requiredScope]);
|
|
209
|
+
if (!scopes.includes(input.requiredScope)) {
|
|
210
|
+
throw new AuthServiceError({
|
|
211
|
+
code: 'invalid_request',
|
|
212
|
+
message: `Missing required scope "${input.requiredScope}".`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
responseType: 'code',
|
|
217
|
+
clientId,
|
|
218
|
+
redirectUri,
|
|
219
|
+
codeChallenge,
|
|
220
|
+
codeMethod,
|
|
221
|
+
scopes,
|
|
222
|
+
...(state === undefined ? {} : { state }),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export function parseOAuthTokenRequest(input) {
|
|
226
|
+
const body = parseBodyToRecord(input.body);
|
|
227
|
+
const allowGrantTypeInference = input.allowGrantTypeInference ?? true;
|
|
228
|
+
const basicClient = parseBasicCredentials(input.authorizationHeader);
|
|
229
|
+
const bodyClientId = toNonEmptyString(body.client_id);
|
|
230
|
+
if (basicClient &&
|
|
231
|
+
bodyClientId !== undefined &&
|
|
232
|
+
basicClient.clientId !== bodyClientId) {
|
|
233
|
+
throw new AuthServiceError({
|
|
234
|
+
code: 'invalid_request',
|
|
235
|
+
message: 'client_id must match between Authorization header and request body.',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const clientId = basicClient?.clientId ?? bodyClientId;
|
|
239
|
+
const clientSecret = basicClient?.clientSecret ?? toNonEmptyString(body.client_secret) ?? null;
|
|
240
|
+
if (clientId === undefined) {
|
|
241
|
+
throw new AuthServiceError({
|
|
242
|
+
code: 'invalid_client',
|
|
243
|
+
message: 'OAuth client credentials are required.',
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
const grantTypeRaw = toNonEmptyString(body.grant_type);
|
|
247
|
+
const inferredGrantType = allowGrantTypeInference && grantTypeRaw === undefined
|
|
248
|
+
? toNonEmptyString(body.code)
|
|
249
|
+
? 'authorization_code'
|
|
250
|
+
: toNonEmptyString(body.refresh_token)
|
|
251
|
+
? 'refresh_token'
|
|
252
|
+
: null
|
|
253
|
+
: null;
|
|
254
|
+
const grantType = (grantTypeRaw ?? inferredGrantType);
|
|
255
|
+
if (!grantType) {
|
|
256
|
+
throw new AuthServiceError({
|
|
257
|
+
code: 'invalid_request',
|
|
258
|
+
message: 'Missing required field "grant_type".',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (grantType === 'authorization_code') {
|
|
262
|
+
return {
|
|
263
|
+
grantType,
|
|
264
|
+
clientId,
|
|
265
|
+
clientSecret,
|
|
266
|
+
authorizationCode: requireField(body, 'code'),
|
|
267
|
+
redirectUri: requireField(body, 'redirect_uri'),
|
|
268
|
+
codeVerifier: requireField(body, 'code_verifier'),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
if (grantType === 'refresh_token') {
|
|
272
|
+
return {
|
|
273
|
+
grantType,
|
|
274
|
+
clientId,
|
|
275
|
+
clientSecret,
|
|
276
|
+
refreshToken: requireField(body, 'refresh_token'),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
if (grantType === 'client_credentials') {
|
|
280
|
+
if (!input.allowClientCredentials) {
|
|
281
|
+
throw new AuthServiceError({
|
|
282
|
+
code: 'invalid_grant',
|
|
283
|
+
message: 'Grant type "client_credentials" is not enabled.',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
grantType,
|
|
288
|
+
clientId,
|
|
289
|
+
clientSecret,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
throw new AuthServiceError({
|
|
293
|
+
code: 'invalid_request',
|
|
294
|
+
message: `Unsupported grant_type "${grantType}".`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
export function appendUrlParams(baseUrl, params) {
|
|
298
|
+
const url = new URL(baseUrl);
|
|
299
|
+
Object.entries(params).forEach(([name, value]) => {
|
|
300
|
+
if (value !== undefined) {
|
|
301
|
+
url.searchParams.set(name, value);
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
return url.toString();
|
|
305
|
+
}
|
|
306
|
+
export function toSingleString(value) {
|
|
307
|
+
return toNonEmptyString(value);
|
|
308
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { OAuthGrantType } from '../../core/adapters/oauth-grant-type';
|
|
2
|
+
export declare function resolveRequestOrigin(input: {
|
|
3
|
+
requestUrl?: string;
|
|
4
|
+
hostHeader?: string;
|
|
5
|
+
forwardedHostHeader?: string;
|
|
6
|
+
forwardedProtoHeader?: string;
|
|
7
|
+
fallbackOrigin?: string;
|
|
8
|
+
}): string;
|
|
9
|
+
export type WellKnownMetadata = {
|
|
10
|
+
oauthProtectedResource: {
|
|
11
|
+
resource: string;
|
|
12
|
+
authorization_servers: string[];
|
|
13
|
+
};
|
|
14
|
+
oauthAuthorizationServer: {
|
|
15
|
+
issuer: string;
|
|
16
|
+
authorization_endpoint: string;
|
|
17
|
+
token_endpoint: string;
|
|
18
|
+
response_types_supported: string[];
|
|
19
|
+
grant_types_supported: OAuthGrantType[];
|
|
20
|
+
code_challenge_methods_supported: Array<'S256' | 'plain'>;
|
|
21
|
+
token_endpoint_auth_methods_supported: string[];
|
|
22
|
+
scopes_supported: string[];
|
|
23
|
+
};
|
|
24
|
+
openidConfiguration: {
|
|
25
|
+
issuer: string;
|
|
26
|
+
authorization_endpoint: string;
|
|
27
|
+
token_endpoint: string;
|
|
28
|
+
response_types_supported: string[];
|
|
29
|
+
grant_types_supported: OAuthGrantType[];
|
|
30
|
+
code_challenge_methods_supported: Array<'S256' | 'plain'>;
|
|
31
|
+
scopes_supported: string[];
|
|
32
|
+
token_endpoint_auth_methods_supported: string[];
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
export declare function buildWellKnownMetadata(input: {
|
|
36
|
+
issuer: string;
|
|
37
|
+
authorizationEndpoint?: string;
|
|
38
|
+
tokenEndpoint?: string;
|
|
39
|
+
protectedResource?: string;
|
|
40
|
+
authorizationServers?: readonly string[];
|
|
41
|
+
responseTypesSupported?: readonly string[];
|
|
42
|
+
grantTypesSupported?: readonly OAuthGrantType[];
|
|
43
|
+
codeChallengeMethodsSupported?: ReadonlyArray<'S256' | 'plain'>;
|
|
44
|
+
tokenEndpointAuthMethodsSupported?: readonly string[];
|
|
45
|
+
scopesSupported?: readonly string[];
|
|
46
|
+
}): WellKnownMetadata;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
function firstHeaderToken(value) {
|
|
2
|
+
if (!value) {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
const first = value.split(',')[0]?.trim();
|
|
6
|
+
return first && first.length > 0 ? first : undefined;
|
|
7
|
+
}
|
|
8
|
+
function normalizeOrigin(value) {
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (trimmed.length === 0) {
|
|
11
|
+
throw new Error('Origin must be a non-empty string.');
|
|
12
|
+
}
|
|
13
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
14
|
+
return new URL(trimmed).origin;
|
|
15
|
+
}
|
|
16
|
+
return new URL(`https://${trimmed}`).origin;
|
|
17
|
+
}
|
|
18
|
+
function normalizePathOrUrl(value) {
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
if (trimmed.length === 0) {
|
|
21
|
+
throw new Error('Path must be a non-empty string.');
|
|
22
|
+
}
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
function resolveAbsoluteUrl(origin, pathOrUrl) {
|
|
26
|
+
const normalized = normalizePathOrUrl(pathOrUrl);
|
|
27
|
+
if (/^https?:\/\//i.test(normalized)) {
|
|
28
|
+
return new URL(normalized).toString();
|
|
29
|
+
}
|
|
30
|
+
const withLeadingSlash = normalized.startsWith('/')
|
|
31
|
+
? normalized
|
|
32
|
+
: `/${normalized}`;
|
|
33
|
+
return new URL(withLeadingSlash, origin).toString();
|
|
34
|
+
}
|
|
35
|
+
export function resolveRequestOrigin(input) {
|
|
36
|
+
if (input.requestUrl) {
|
|
37
|
+
try {
|
|
38
|
+
return new URL(input.requestUrl).origin;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Falls through to header-based resolution.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const forwardedHost = firstHeaderToken(input.forwardedHostHeader);
|
|
45
|
+
const forwardedProto = firstHeaderToken(input.forwardedProtoHeader);
|
|
46
|
+
if (forwardedHost) {
|
|
47
|
+
const protocol = forwardedProto ?? 'https';
|
|
48
|
+
return normalizeOrigin(`${protocol}://${forwardedHost}`);
|
|
49
|
+
}
|
|
50
|
+
const host = firstHeaderToken(input.hostHeader);
|
|
51
|
+
if (host) {
|
|
52
|
+
const protocol = forwardedProto ??
|
|
53
|
+
(host.includes('localhost') || host.startsWith('127.')
|
|
54
|
+
? 'http'
|
|
55
|
+
: 'https');
|
|
56
|
+
return normalizeOrigin(`${protocol}://${host}`);
|
|
57
|
+
}
|
|
58
|
+
return normalizeOrigin(input.fallbackOrigin ?? 'http://localhost');
|
|
59
|
+
}
|
|
60
|
+
export function buildWellKnownMetadata(input) {
|
|
61
|
+
const issuer = normalizeOrigin(input.issuer);
|
|
62
|
+
const authorizationEndpoint = resolveAbsoluteUrl(issuer, input.authorizationEndpoint ?? '/authorize');
|
|
63
|
+
const tokenEndpoint = resolveAbsoluteUrl(issuer, input.tokenEndpoint ?? '/token');
|
|
64
|
+
const protectedResource = resolveAbsoluteUrl(issuer, input.protectedResource ?? '/');
|
|
65
|
+
const authorizationServers = (input.authorizationServers ?? [issuer]).map(server => normalizeOrigin(server));
|
|
66
|
+
const responseTypesSupported = [
|
|
67
|
+
...(input.responseTypesSupported ?? ['code']),
|
|
68
|
+
];
|
|
69
|
+
const grantTypesSupported = [
|
|
70
|
+
...(input.grantTypesSupported ?? ['authorization_code', 'refresh_token']),
|
|
71
|
+
];
|
|
72
|
+
const codeChallengeMethodsSupported = [
|
|
73
|
+
...(input.codeChallengeMethodsSupported ?? ['S256', 'plain']),
|
|
74
|
+
];
|
|
75
|
+
const tokenEndpointAuthMethodsSupported = [
|
|
76
|
+
...(input.tokenEndpointAuthMethodsSupported ?? [
|
|
77
|
+
'client_secret_basic',
|
|
78
|
+
'client_secret_post',
|
|
79
|
+
]),
|
|
80
|
+
];
|
|
81
|
+
const scopesSupported = [...(input.scopesSupported ?? ['mcp:access'])];
|
|
82
|
+
return {
|
|
83
|
+
oauthProtectedResource: {
|
|
84
|
+
resource: protectedResource,
|
|
85
|
+
authorization_servers: authorizationServers,
|
|
86
|
+
},
|
|
87
|
+
oauthAuthorizationServer: {
|
|
88
|
+
issuer,
|
|
89
|
+
authorization_endpoint: authorizationEndpoint,
|
|
90
|
+
token_endpoint: tokenEndpoint,
|
|
91
|
+
response_types_supported: responseTypesSupported,
|
|
92
|
+
grant_types_supported: grantTypesSupported,
|
|
93
|
+
code_challenge_methods_supported: codeChallengeMethodsSupported,
|
|
94
|
+
token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported,
|
|
95
|
+
scopes_supported: scopesSupported,
|
|
96
|
+
},
|
|
97
|
+
openidConfiguration: {
|
|
98
|
+
issuer,
|
|
99
|
+
authorization_endpoint: authorizationEndpoint,
|
|
100
|
+
token_endpoint: tokenEndpoint,
|
|
101
|
+
response_types_supported: responseTypesSupported,
|
|
102
|
+
grant_types_supported: grantTypesSupported,
|
|
103
|
+
code_challenge_methods_supported: codeChallengeMethodsSupported,
|
|
104
|
+
token_endpoint_auth_methods_supported: tokenEndpointAuthMethodsSupported,
|
|
105
|
+
scopes_supported: scopesSupported,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function createInMemoryAccessTokenRevocationAdapter() {
|
|
2
|
+
const revokedJtis = new Map();
|
|
3
|
+
return {
|
|
4
|
+
async revokeJti(input) {
|
|
5
|
+
revokedJtis.set(input.jti, {
|
|
6
|
+
expiresAtUnix: input.expiresAtUnix,
|
|
7
|
+
...(input.reason === undefined ? {} : { reason: input.reason }),
|
|
8
|
+
});
|
|
9
|
+
},
|
|
10
|
+
async isRevoked(jti) {
|
|
11
|
+
return revokedJtis.has(jti);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function createInMemoryAuthorizationCodeAdapter() {
|
|
2
|
+
const records = new Map();
|
|
3
|
+
return {
|
|
4
|
+
async create(input) {
|
|
5
|
+
records.set(input.codeHash, {
|
|
6
|
+
userId: input.userId,
|
|
7
|
+
codeHash: input.codeHash,
|
|
8
|
+
clientId: input.clientId,
|
|
9
|
+
redirectUri: input.redirectUri,
|
|
10
|
+
codeChallenge: input.codeChallenge,
|
|
11
|
+
codeMethod: input.codeMethod,
|
|
12
|
+
expiresAtUnix: input.expiresAtUnix,
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
async consume(codeHash, nowUnix) {
|
|
16
|
+
const record = records.get(codeHash);
|
|
17
|
+
if (!record) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (record.consumedAtUnix === undefined ||
|
|
21
|
+
record.consumedAtUnix === null) {
|
|
22
|
+
record.consumedAtUnix = nowUnix;
|
|
23
|
+
records.set(codeHash, record);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
userId: record.userId,
|
|
27
|
+
clientId: record.clientId,
|
|
28
|
+
redirectUri: record.redirectUri,
|
|
29
|
+
codeChallenge: record.codeChallenge,
|
|
30
|
+
codeMethod: record.codeMethod,
|
|
31
|
+
expiresAtUnix: record.expiresAtUnix,
|
|
32
|
+
consumedAtUnix: record.consumedAtUnix,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { OAuthClientAdapter } from '../../core/adapters/oauth-client-adapter';
|
|
2
|
+
import type { OAuthGrantType } from '../../core/adapters/oauth-grant-type';
|
|
3
|
+
import type { AuthActor } from '../../core/types/auth-contract';
|
|
4
|
+
export type InMemoryOAuthClient<TActor extends AuthActor, TUserId> = {
|
|
5
|
+
clientId: string;
|
|
6
|
+
clientSecret: string | null;
|
|
7
|
+
actor: TActor;
|
|
8
|
+
userId: TUserId;
|
|
9
|
+
allowedGrantTypes?: readonly OAuthGrantType[];
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function createInMemoryOAuthClientAdapter<TActor extends AuthActor, TUserId>(input: {
|
|
13
|
+
clients: readonly InMemoryOAuthClient<TActor, TUserId>[];
|
|
14
|
+
}): OAuthClientAdapter<TActor, TUserId>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function createInMemoryOAuthClientAdapter(input) {
|
|
2
|
+
const clientsById = new Map();
|
|
3
|
+
for (const client of input.clients) {
|
|
4
|
+
clientsById.set(client.clientId, client);
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
async validateClient(request) {
|
|
8
|
+
const client = clientsById.get(request.clientId);
|
|
9
|
+
if (!client || client.disabled) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (client.clientSecret !== request.clientSecret) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (client.allowedGrantTypes !== undefined &&
|
|
16
|
+
!client.allowedGrantTypes.includes(request.grantType)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
clientId: client.clientId,
|
|
21
|
+
actor: client.actor,
|
|
22
|
+
userId: client.userId,
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function createInMemoryPendingAuthRequestAdapter() {
|
|
2
|
+
const records = new Map();
|
|
3
|
+
return {
|
|
4
|
+
async create(input) {
|
|
5
|
+
records.set(input.requestId, {
|
|
6
|
+
payload: structuredClone(input.payload),
|
|
7
|
+
expiresAtUnix: input.expiresAtUnix,
|
|
8
|
+
});
|
|
9
|
+
},
|
|
10
|
+
async find(requestId) {
|
|
11
|
+
const record = records.get(requestId);
|
|
12
|
+
if (!record) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
payload: structuredClone(record.payload),
|
|
17
|
+
expiresAtUnix: record.expiresAtUnix,
|
|
18
|
+
...(record.consumedAtUnix === undefined
|
|
19
|
+
? {}
|
|
20
|
+
: { consumedAtUnix: record.consumedAtUnix }),
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
async consume(requestId, nowUnix) {
|
|
24
|
+
const record = records.get(requestId);
|
|
25
|
+
if (!record) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (record.consumedAtUnix === undefined ||
|
|
29
|
+
record.consumedAtUnix === null) {
|
|
30
|
+
record.consumedAtUnix = nowUnix;
|
|
31
|
+
records.set(requestId, record);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
payload: structuredClone(record.payload),
|
|
35
|
+
expiresAtUnix: record.expiresAtUnix,
|
|
36
|
+
consumedAtUnix: record.consumedAtUnix,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
async delete(requestId) {
|
|
40
|
+
records.delete(requestId);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|