@atlasauth/emulator-core 0.1.0 → 0.2.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/dist/adopt.d.ts +50 -0
- package/dist/adopt.js +57 -0
- package/dist/adopt.js.map +1 -0
- package/dist/coverage.d.ts +15 -0
- package/dist/coverage.js +31 -0
- package/dist/coverage.js.map +1 -0
- package/dist/crypto-util.d.ts +10 -0
- package/dist/crypto-util.js +76 -0
- package/dist/crypto-util.js.map +1 -0
- package/dist/engine.d.ts +33 -2
- package/dist/engine.js +53 -8
- package/dist/engine.js.map +1 -1
- package/dist/error-catalog.js +8 -0
- package/dist/error-catalog.js.map +1 -1
- package/dist/event-gate.d.ts +3 -0
- package/dist/event-gate.js +372 -0
- package/dist/event-gate.js.map +1 -0
- package/dist/generic.js +9 -1
- package/dist/generic.js.map +1 -1
- package/dist/handlers/approvals.d.ts +2 -0
- package/dist/handlers/approvals.js +365 -0
- package/dist/handlers/approvals.js.map +1 -0
- package/dist/handlers/authz.d.ts +2 -0
- package/dist/handlers/authz.js +347 -0
- package/dist/handlers/authz.js.map +1 -0
- package/dist/handlers/billing.d.ts +2 -0
- package/dist/handlers/billing.js +398 -0
- package/dist/handlers/billing.js.map +1 -0
- package/dist/handlers/connections.d.ts +2 -0
- package/dist/handlers/connections.js +332 -0
- package/dist/handlers/connections.js.map +1 -0
- package/dist/handlers/device.d.ts +47 -0
- package/dist/handlers/device.js +648 -0
- package/dist/handlers/device.js.map +1 -0
- package/dist/handlers/federation.d.ts +2 -0
- package/dist/handlers/federation.js +481 -0
- package/dist/handlers/federation.js.map +1 -0
- package/dist/handlers/governance.d.ts +2 -0
- package/dist/handlers/governance.js +460 -0
- package/dist/handlers/governance.js.map +1 -0
- package/dist/handlers/hooks.js +4 -1
- package/dist/handlers/hooks.js.map +1 -1
- package/dist/handlers/index.d.ts +5 -1
- package/dist/handlers/index.js +32 -3
- package/dist/handlers/index.js.map +1 -1
- package/dist/handlers/keys.js +6 -1
- package/dist/handlers/keys.js.map +1 -1
- package/dist/handlers/machines.js +55 -8
- package/dist/handlers/machines.js.map +1 -1
- package/dist/handlers/messaging.d.ts +4 -0
- package/dist/handlers/messaging.js +486 -0
- package/dist/handlers/messaging.js.map +1 -0
- package/dist/handlers/oauth-flows.d.ts +28 -0
- package/dist/handlers/oauth-flows.js +419 -0
- package/dist/handlers/oauth-flows.js.map +1 -0
- package/dist/handlers/oauth.d.ts +75 -0
- package/dist/handlers/oauth.js +608 -0
- package/dist/handlers/oauth.js.map +1 -0
- package/dist/handlers/passkeys.d.ts +3 -0
- package/dist/handlers/passkeys.js +293 -0
- package/dist/handlers/passkeys.js.map +1 -0
- package/dist/handlers/platform.d.ts +15 -0
- package/dist/handlers/platform.js +454 -0
- package/dist/handlers/platform.js.map +1 -0
- package/dist/handlers/round2.d.ts +8 -0
- package/dist/handlers/round2.js +372 -0
- package/dist/handlers/round2.js.map +1 -0
- package/dist/handlers/scim.d.ts +2 -0
- package/dist/handlers/scim.js +408 -0
- package/dist/handlers/scim.js.map +1 -0
- package/dist/handlers/sessions.d.ts +7 -3
- package/dist/handlers/sessions.js +55 -25
- package/dist/handlers/sessions.js.map +1 -1
- package/dist/handlers/support.d.ts +20 -0
- package/dist/handlers/support.js +62 -0
- package/dist/handlers/support.js.map +1 -0
- package/dist/handlers/users.js +4 -0
- package/dist/handlers/users.js.map +1 -1
- package/dist/handlers/workflows.d.ts +2 -0
- package/dist/handlers/workflows.js +428 -0
- package/dist/handlers/workflows.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +23 -1
- package/dist/index.js.map +1 -1
- package/dist/jws.d.ts +41 -0
- package/dist/jws.js +133 -0
- package/dist/jws.js.map +1 -0
- package/dist/messages.d.ts +7 -0
- package/dist/messages.js +8 -4
- package/dist/messages.js.map +1 -1
- package/dist/parity-bin.js +3 -1
- package/dist/parity-bin.js.map +1 -1
- package/dist/parity-routes.d.ts +14 -0
- package/dist/parity-routes.js +421 -0
- package/dist/parity-routes.js.map +1 -0
- package/dist/parity.d.ts +10 -3
- package/dist/parity.js +14 -5
- package/dist/parity.js.map +1 -1
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.js +46 -0
- package/dist/tokens.js.map +1 -1
- package/dist/webauthn-virtual.d.ts +70 -0
- package/dist/webauthn-virtual.js +186 -0
- package/dist/webauthn-virtual.js.map +1 -0
- package/dist/webhook-ext.d.ts +16 -0
- package/dist/webhook-ext.js +218 -0
- package/dist/webhook-ext.js.map +1 -0
- package/dist/webhooks.d.ts +3 -0
- package/dist/webhooks.js +37 -3
- package/dist/webhooks.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.splitScope = exports.fail = exports.resetOAuth = exports.st = void 0;
|
|
4
|
+
exports.discoveryDocument = discoveryDocument;
|
|
5
|
+
exports.authenticateClient = authenticateClient;
|
|
6
|
+
exports.mintAccess = mintAccess;
|
|
7
|
+
exports.issueTokens = issueTokens;
|
|
8
|
+
exports.issueCode = issueCode;
|
|
9
|
+
exports.oauthHandlers = oauthHandlers;
|
|
10
|
+
const crypto_util_1 = require("../crypto-util");
|
|
11
|
+
const errors_1 = require("../errors");
|
|
12
|
+
const openapi_1 = require("../openapi");
|
|
13
|
+
const store_1 = require("../store");
|
|
14
|
+
const jws_1 = require("../jws");
|
|
15
|
+
const oauth_flows_1 = require("./oauth-flows");
|
|
16
|
+
const util_1 = require("./util");
|
|
17
|
+
/**
|
|
18
|
+
* The OAuth 2.0 / OpenID Connect provider: client registry (`/v1/oauth_clients`), discovery, authorize (code + PKCE
|
|
19
|
+
* S256), token (authorization_code, refresh_token with rotation and reuse detection, client_credentials, token
|
|
20
|
+
* exchange RFC 8693), revoke (RFC 7009), introspect (RFC 7662), userinfo and the JWKS. Tokens are real signed JWTs
|
|
21
|
+
* (access and ID tokens) verifiable against `/.well-known/jwks.json`. Errors follow RFC 6749 section 5.2.
|
|
22
|
+
*/
|
|
23
|
+
const CODE_TTL_MS = 10 * 60_000;
|
|
24
|
+
const ACCESS_TTL_S = 3600;
|
|
25
|
+
const GRACE_MS = 30_000;
|
|
26
|
+
const EXCHANGE = 'urn:ietf:params:oauth:grant-type:token-exchange';
|
|
27
|
+
const ACCESS_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
|
|
28
|
+
const SCOPES = ['openid', 'profile', 'email', 'offline_access'];
|
|
29
|
+
const states = new WeakMap();
|
|
30
|
+
const st = (e) => {
|
|
31
|
+
let s = states.get(e);
|
|
32
|
+
if (!s)
|
|
33
|
+
states.set(e, (s = { codes: new Map(), grants: new Map(), refresh: new Map(), access: new Map(), autoUser: null }));
|
|
34
|
+
return s;
|
|
35
|
+
};
|
|
36
|
+
exports.st = st;
|
|
37
|
+
const resetOAuth = (e) => {
|
|
38
|
+
states.delete(e);
|
|
39
|
+
(0, oauth_flows_1.resetOAuthFlows)(e);
|
|
40
|
+
};
|
|
41
|
+
exports.resetOAuth = resetOAuth;
|
|
42
|
+
const fail = (status, error, description, extra = {}) => ({ status, body: { error, error_description: description, ...extra }, headers: { 'cache-control': 'no-store', pragma: 'no-cache' } });
|
|
43
|
+
exports.fail = fail;
|
|
44
|
+
function discoveryDocument(e) {
|
|
45
|
+
const iss = e.issuer;
|
|
46
|
+
return {
|
|
47
|
+
issuer: iss,
|
|
48
|
+
jwks_uri: `${iss}/.well-known/jwks.json`,
|
|
49
|
+
authorization_endpoint: `${iss}/oauth2/authorize`,
|
|
50
|
+
token_endpoint: `${iss}/oauth2/token`,
|
|
51
|
+
userinfo_endpoint: `${iss}/oauth2/userinfo`,
|
|
52
|
+
revocation_endpoint: `${iss}/oauth2/revoke`,
|
|
53
|
+
introspection_endpoint: `${iss}/oauth2/introspect`,
|
|
54
|
+
response_types_supported: ['code'],
|
|
55
|
+
response_modes_supported: ['query'],
|
|
56
|
+
grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials', EXCHANGE, 'urn:ietf:params:oauth:grant-type:device_code', 'urn:openid:params:grant-type:ciba'],
|
|
57
|
+
subject_types_supported: ['public'],
|
|
58
|
+
id_token_signing_alg_values_supported: ['RS256', 'ES256'],
|
|
59
|
+
token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'],
|
|
60
|
+
code_challenge_methods_supported: ['S256'],
|
|
61
|
+
dpop_signing_alg_values_supported: ['ES256', 'ES384', 'RS256', 'PS256', 'EdDSA'],
|
|
62
|
+
tls_client_certificate_bound_access_tokens: true,
|
|
63
|
+
pushed_authorization_request_endpoint: `${iss}/oauth2/par`,
|
|
64
|
+
device_authorization_endpoint: `${iss}/oauth2/device_authorization`,
|
|
65
|
+
backchannel_authentication_endpoint: `${iss}/oauth2/backchannel_authentication`,
|
|
66
|
+
backchannel_token_delivery_modes_supported: ['poll', 'ping'],
|
|
67
|
+
scopes_supported: SCOPES,
|
|
68
|
+
claims_supported: ['sub', 'iss', 'aud', 'exp', 'iat', 'auth_time', 'nonce', 'email', 'email_verified', 'name', 'picture'],
|
|
69
|
+
authorization_response_iss_parameter_supported: true,
|
|
70
|
+
atlas_emulator: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const FLOW_FLAGS = ['require_consent', 'dpop_bound_access_tokens', 'tls_client_certificate_bound_access_tokens', 'require_pushed_authorization_requests'];
|
|
74
|
+
/** The client settings the consent, binding, PAR and CIBA flows read; validated here so a bad value never reaches a flow. */
|
|
75
|
+
function pickFlowFields(body) {
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const k of FLOW_FLAGS) {
|
|
78
|
+
if (body[k] === undefined)
|
|
79
|
+
continue;
|
|
80
|
+
if (typeof body[k] !== 'boolean')
|
|
81
|
+
throw new errors_1.EmulatorError('VALIDATION_FAILED', `${k} must be true or false.`, k);
|
|
82
|
+
out[k] = body[k];
|
|
83
|
+
}
|
|
84
|
+
if (body.backchannel_token_delivery_mode !== undefined) {
|
|
85
|
+
if (!['poll', 'ping'].includes(body.backchannel_token_delivery_mode))
|
|
86
|
+
throw new errors_1.EmulatorError('VALIDATION_FAILED', 'backchannel_token_delivery_mode must be poll or ping.', 'backchannel_token_delivery_mode');
|
|
87
|
+
out.backchannel_token_delivery_mode = body.backchannel_token_delivery_mode;
|
|
88
|
+
}
|
|
89
|
+
if (body.backchannel_client_notification_endpoint !== undefined)
|
|
90
|
+
out.backchannel_client_notification_endpoint = body.backchannel_client_notification_endpoint;
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
const clientShape = (e, c, secret) => {
|
|
94
|
+
const op = e.index.ops.find((o) => o.method === 'POST' && o.path === '/v1/oauth_clients');
|
|
95
|
+
const base = op?.successSchema ? (0, openapi_1.synthesize)(e.index, op.successSchema) : {};
|
|
96
|
+
return {
|
|
97
|
+
...(base && typeof base === 'object' ? base : {}),
|
|
98
|
+
object: 'oauth_client',
|
|
99
|
+
id: c.id,
|
|
100
|
+
client_id: c.id,
|
|
101
|
+
name: c.name,
|
|
102
|
+
redirect_uris: c.redirect_uris,
|
|
103
|
+
allowed_scopes: c.allowed_scopes,
|
|
104
|
+
grant_types: c.grant_types,
|
|
105
|
+
token_endpoint_auth_method: c.auth_method,
|
|
106
|
+
first_party: c.first_party === true,
|
|
107
|
+
post_logout_redirect_uris: c.post_logout_redirect_uris ?? [],
|
|
108
|
+
require_consent: c.require_consent === true,
|
|
109
|
+
dpop_bound_access_tokens: c.dpop_bound_access_tokens === true,
|
|
110
|
+
tls_client_certificate_bound_access_tokens: c.tls_client_certificate_bound_access_tokens === true,
|
|
111
|
+
require_pushed_authorization_requests: c.require_pushed_authorization_requests === true,
|
|
112
|
+
backchannel_token_delivery_mode: c.backchannel_token_delivery_mode ?? 'poll',
|
|
113
|
+
backchannel_client_notification_endpoint: c.backchannel_client_notification_endpoint ?? null,
|
|
114
|
+
created_at: c.created_at,
|
|
115
|
+
updated_at: c.updated_at,
|
|
116
|
+
...(secret ? { client_secret: secret } : {}),
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
const liveClient = (e, id) => {
|
|
120
|
+
const c = e.db.oauthClients.get(id);
|
|
121
|
+
if (!c || c.deleted)
|
|
122
|
+
throw (0, errors_1.notFound)('OAuth client');
|
|
123
|
+
return c;
|
|
124
|
+
};
|
|
125
|
+
/** Client authentication: Basic, or client_id (+ client_secret) in the body. `none` clients are identified only. */
|
|
126
|
+
function authenticateClient(e, c) {
|
|
127
|
+
let id = typeof c.body.client_id === 'string' ? c.body.client_id : undefined;
|
|
128
|
+
let secret = typeof c.body.client_secret === 'string' ? c.body.client_secret : undefined;
|
|
129
|
+
const basic = /^Basic\s+(.+)$/i.exec(c.request.headers.get('authorization') ?? '')?.[1];
|
|
130
|
+
if (basic) {
|
|
131
|
+
const [i, ...rest] = atob(basic).split(':');
|
|
132
|
+
id = decodeURIComponent(i ?? '');
|
|
133
|
+
secret = decodeURIComponent(rest.join(':'));
|
|
134
|
+
}
|
|
135
|
+
const client = e.db.oauthClients.get(id);
|
|
136
|
+
if (!client || client.deleted)
|
|
137
|
+
return { error: (0, exports.fail)(401, 'invalid_client', 'Unknown client.') };
|
|
138
|
+
const previousOk = client.previous_secret !== undefined && client.previous_secret === secret && e.clock.now() < client.previous_until;
|
|
139
|
+
if (client.auth_method !== 'none' && client.secret !== secret && !previousOk)
|
|
140
|
+
return { error: (0, exports.fail)(401, 'invalid_client', 'Client authentication failed.') };
|
|
141
|
+
return { client };
|
|
142
|
+
}
|
|
143
|
+
const splitScope = (s) => (typeof s === 'string' ? s.split(/\s+/).filter(Boolean) : []);
|
|
144
|
+
exports.splitScope = splitScope;
|
|
145
|
+
async function mintAccess(e, input) {
|
|
146
|
+
const jti = e.db.newId('oat');
|
|
147
|
+
const token = await e.tokens.signClaims({
|
|
148
|
+
sub: input.userId ?? input.client.id,
|
|
149
|
+
aud: input.audience ?? input.client.id,
|
|
150
|
+
client_id: input.client.id,
|
|
151
|
+
scope: input.scope.join(' '),
|
|
152
|
+
jti,
|
|
153
|
+
token_use: 'access_token',
|
|
154
|
+
...(input.cnf ? { cnf: input.cnf } : {}),
|
|
155
|
+
...(input.extra ?? {}),
|
|
156
|
+
}, ACCESS_TTL_S);
|
|
157
|
+
const s = (0, exports.st)(e);
|
|
158
|
+
s.access.set(jti, { clientId: input.client.id, userId: input.userId, scope: input.scope, exp: e.clock.now() + ACCESS_TTL_S * 1000, revoked: false, grantId: input.grant?.id ?? null, cnf: input.cnf });
|
|
159
|
+
input.grant?.jtis.add(jti);
|
|
160
|
+
return { token, jti };
|
|
161
|
+
}
|
|
162
|
+
const userClaims = (e, userId, scope) => {
|
|
163
|
+
const u = e.db.users.get(userId);
|
|
164
|
+
if (!u)
|
|
165
|
+
return {};
|
|
166
|
+
const primary = u.emails.find((m) => m.id === u.primary_email_address_id);
|
|
167
|
+
return {
|
|
168
|
+
...(scope.includes('email') && primary ? { email: primary.email_address, email_verified: primary.verified === true } : {}),
|
|
169
|
+
...(scope.includes('profile') ? { name: [u.first_name, u.last_name].filter(Boolean).join(' ') || undefined, picture: u.image_url ?? undefined, given_name: u.first_name ?? undefined, family_name: u.last_name ?? undefined } : {}),
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
async function issueTokens(e, client, grant, nonce, authTime, refreshRec, cnf) {
|
|
173
|
+
const access = await mintAccess(e, { client, userId: grant.userId, scope: grant.scope, grant, cnf });
|
|
174
|
+
const out = { access_token: access.token, token_type: cnf?.jkt ? 'DPoP' : 'Bearer', expires_in: ACCESS_TTL_S, scope: grant.scope.join(' ') };
|
|
175
|
+
if (grant.scope.includes('offline_access') || client.grant_types.includes('refresh_token')) {
|
|
176
|
+
const rt = refreshRec ?? `ort_${(0, store_1.randomToken)(32)}`;
|
|
177
|
+
if (!(0, exports.st)(e).refresh.has(rt))
|
|
178
|
+
(0, exports.st)(e).refresh.set(rt, { grantId: grant.id });
|
|
179
|
+
out.refresh_token = rt;
|
|
180
|
+
}
|
|
181
|
+
if (grant.scope.includes('openid') && grant.userId) {
|
|
182
|
+
out.id_token = await e.tokens.signClaims({ sub: grant.userId, aud: client.id, azp: client.id, auth_time: Math.floor(authTime / 1000), ...(nonce ? { nonce } : {}), token_use: 'id_token', ...userClaims(e, grant.userId, grant.scope) }, ACCESS_TTL_S);
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
185
|
+
}
|
|
186
|
+
/** Create the grant and the single-use authorization code for an approved request (after login and, when needed, consent). */
|
|
187
|
+
function issueCode(e, a) {
|
|
188
|
+
const grant = { id: e.db.newId('oag'), clientId: a.client.id, userId: a.userId, scope: a.scope, revoked: false, jtis: new Set() };
|
|
189
|
+
(0, exports.st)(e).grants.set(grant.id, grant);
|
|
190
|
+
const code = `oc_${(0, store_1.randomToken)(24)}`;
|
|
191
|
+
const now = e.clock.now();
|
|
192
|
+
(0, exports.st)(e).codes.set(code, { grantId: grant.id, redirectUri: a.redirectUri, challenge: a.challenge, nonce: a.nonce, authTime: now, expiresAt: now + CODE_TTL_MS, used: false });
|
|
193
|
+
return code;
|
|
194
|
+
}
|
|
195
|
+
/** What one token request proved: a DPoP key (single-use proof) and, for clients that ask for it, a client certificate. */
|
|
196
|
+
async function provedOf(c, client) {
|
|
197
|
+
const e = c.engine;
|
|
198
|
+
const proved = {};
|
|
199
|
+
const header = c.request.headers.get('dpop');
|
|
200
|
+
if (header !== null) {
|
|
201
|
+
try {
|
|
202
|
+
const r = await (0, jws_1.verifyDpopProof)({ proof: header, htm: 'POST', htu: c.request.url, nowMs: e.clock.now() });
|
|
203
|
+
if (!(0, oauth_flows_1.claimDpopJti)(e, r.jkt, r.jti, (jws_1.DPOP_IAT_WINDOW_S + 5) * 1000))
|
|
204
|
+
return { error: (0, exports.fail)(400, 'invalid_dpop_proof', 'The DPoP proof has already been used.') };
|
|
205
|
+
proved.jkt = r.jkt;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return { error: (0, exports.fail)(400, 'invalid_dpop_proof', err.message) };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const x5t = await (0, jws_1.forwardedCertThumbprint)(c.request.headers);
|
|
212
|
+
if (x5t === null)
|
|
213
|
+
return { error: (0, exports.fail)(400, 'invalid_request', 'The forwarded client certificate could not be read.') };
|
|
214
|
+
if (x5t && client.tls_client_certificate_bound_access_tokens === true)
|
|
215
|
+
proved.x5t = x5t;
|
|
216
|
+
if (client.dpop_bound_access_tokens === true && !proved.jkt)
|
|
217
|
+
return { error: (0, exports.fail)(400, 'invalid_dpop_proof', 'This client must send a DPoP proof with every token request.') };
|
|
218
|
+
if (client.tls_client_certificate_bound_access_tokens === true && !proved.x5t)
|
|
219
|
+
return { error: (0, exports.fail)(401, 'invalid_client', 'This client must present its client certificate (mutual TLS).') };
|
|
220
|
+
return { proved };
|
|
221
|
+
}
|
|
222
|
+
const cnfFor = (p) => (p.jkt || p.x5t ? { ...(p.jkt ? { jkt: p.jkt } : {}), ...(p.x5t ? { 'x5t#S256': p.x5t } : {}) } : undefined);
|
|
223
|
+
function oauthHandlers() {
|
|
224
|
+
const redirect = (uri, params) => {
|
|
225
|
+
const u = new URL(uri);
|
|
226
|
+
for (const [k, v] of Object.entries(params))
|
|
227
|
+
if (v !== undefined)
|
|
228
|
+
u.searchParams.set(k, v);
|
|
229
|
+
return { status: 302, headers: { location: u.toString(), 'cache-control': 'no-store' } };
|
|
230
|
+
};
|
|
231
|
+
return [
|
|
232
|
+
// ── client registry ──
|
|
233
|
+
{
|
|
234
|
+
method: 'POST',
|
|
235
|
+
pattern: '/v1/oauth_clients',
|
|
236
|
+
handler: (c) => {
|
|
237
|
+
const e = c.engine;
|
|
238
|
+
const name = String((0, util_1.need)(c.body, 'name'));
|
|
239
|
+
const uris = c.body.redirect_uris;
|
|
240
|
+
if (!Array.isArray(uris) || !uris.length || !uris.every((u) => typeof u === 'string'))
|
|
241
|
+
throw new errors_1.EmulatorError('VALIDATION_FAILED', 'redirect_uris needs at least one URI.', 'redirect_uris');
|
|
242
|
+
const method = c.body.token_endpoint_auth_method ?? 'client_secret_basic';
|
|
243
|
+
const now = e.clock.now();
|
|
244
|
+
const secret = method === 'none' ? undefined : `oacs_${(0, store_1.randomToken)(32)}`;
|
|
245
|
+
const row = e.db.oauthClients.insert({
|
|
246
|
+
name,
|
|
247
|
+
redirect_uris: uris,
|
|
248
|
+
allowed_scopes: c.body.allowed_scopes ?? SCOPES,
|
|
249
|
+
grant_types: c.body.grant_types ?? ['authorization_code', 'refresh_token'],
|
|
250
|
+
auth_method: method,
|
|
251
|
+
first_party: c.body.first_party === true,
|
|
252
|
+
post_logout_redirect_uris: c.body.post_logout_redirect_uris ?? [],
|
|
253
|
+
...pickFlowFields(c.body),
|
|
254
|
+
secret,
|
|
255
|
+
created_at: now,
|
|
256
|
+
updated_at: now,
|
|
257
|
+
});
|
|
258
|
+
e.changed({ type: 'oauth_client.created', data: clientShape(e, row) });
|
|
259
|
+
return { status: 201, body: clientShape(e, row, secret) };
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{ method: 'GET', pattern: '/v1/oauth_clients', handler: (c) => (0, util_1.listReply)(c, c.engine.db.oauthClients.filter((x) => !x.deleted), (x) => clientShape(c.engine, x)) },
|
|
263
|
+
{ method: 'GET', pattern: '/v1/oauth_clients/:id', handler: (c) => ({ body: clientShape(c.engine, liveClient(c.engine, c.params.id)) }) },
|
|
264
|
+
{
|
|
265
|
+
method: 'PATCH',
|
|
266
|
+
pattern: '/v1/oauth_clients/:id',
|
|
267
|
+
handler: (c) => {
|
|
268
|
+
const e = c.engine;
|
|
269
|
+
const row = liveClient(e, c.params.id);
|
|
270
|
+
for (const [k, to] of [['name', 'name'], ['redirect_uris', 'redirect_uris'], ['allowed_scopes', 'allowed_scopes'], ['grant_types', 'grant_types'], ['first_party', 'first_party'], ['post_logout_redirect_uris', 'post_logout_redirect_uris']])
|
|
271
|
+
if (k in c.body)
|
|
272
|
+
row[to] = c.body[k];
|
|
273
|
+
Object.assign(row, pickFlowFields(c.body));
|
|
274
|
+
row.updated_at = e.clock.now();
|
|
275
|
+
e.changed({ type: 'oauth_client.updated', data: clientShape(e, row) });
|
|
276
|
+
return { body: clientShape(e, row) };
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
method: 'DELETE',
|
|
281
|
+
pattern: '/v1/oauth_clients/:id',
|
|
282
|
+
handler: (c) => {
|
|
283
|
+
const e = c.engine;
|
|
284
|
+
const row = liveClient(e, c.params.id);
|
|
285
|
+
row.deleted = true;
|
|
286
|
+
// Deleting a client ends everything it was issued.
|
|
287
|
+
for (const g of (0, exports.st)(e).grants.values())
|
|
288
|
+
if (g.clientId === row.id)
|
|
289
|
+
g.revoked = true;
|
|
290
|
+
for (const a of (0, exports.st)(e).access.values())
|
|
291
|
+
if (a.clientId === row.id)
|
|
292
|
+
a.revoked = true;
|
|
293
|
+
e.changed({ type: 'oauth_client.deleted', data: { object: 'oauth_client', id: row.id, deleted: true } });
|
|
294
|
+
return { body: { object: 'oauth_client', id: row.id, deleted: true } };
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
method: 'POST',
|
|
299
|
+
pattern: '/v1/oauth_clients/:id/rotate_secret',
|
|
300
|
+
handler: (c) => {
|
|
301
|
+
const e = c.engine;
|
|
302
|
+
const row = liveClient(e, c.params.id);
|
|
303
|
+
if (row.auth_method === 'none')
|
|
304
|
+
throw new errors_1.EmulatorError('VALIDATION_FAILED', 'A public client has no secret.');
|
|
305
|
+
// The previous secret keeps working for overlap_seconds (default 24 h, 0 revokes it now), like the real API.
|
|
306
|
+
const overlap = typeof c.body.overlap_seconds === 'number' ? c.body.overlap_seconds : 86_400;
|
|
307
|
+
row.previous_secret = overlap > 0 ? row.secret : undefined;
|
|
308
|
+
row.previous_until = e.clock.now() + overlap * 1000;
|
|
309
|
+
row.secret = `oacs_${(0, store_1.randomToken)(32)}`;
|
|
310
|
+
row.updated_at = e.clock.now();
|
|
311
|
+
e.changed({ type: 'oauth_client.secret_rotated', data: clientShape(e, row) });
|
|
312
|
+
return { body: clientShape(e, row, row.secret) };
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
// ── discovery ──
|
|
316
|
+
{ method: 'GET', pattern: '/.well-known/oauth-authorization-server', public: true, handler: (c) => ({ body: discoveryDocument(c.engine) }) },
|
|
317
|
+
// ── authorize ──
|
|
318
|
+
{
|
|
319
|
+
// The emulator is its own user agent: it signs in the `login_hint` user (id or email), else the user set at
|
|
320
|
+
// `POST /__emulator/oauth/login`, else the only user, and approves without a consent screen. `emulator_decision=deny`
|
|
321
|
+
// answers access_denied, `prompt=none` with nobody to sign in answers login_required.
|
|
322
|
+
method: 'GET',
|
|
323
|
+
pattern: '/oauth2/authorize',
|
|
324
|
+
public: true,
|
|
325
|
+
handler: (c) => {
|
|
326
|
+
const e = c.engine;
|
|
327
|
+
const resolved = (0, oauth_flows_1.resolveRequestUri)(e, c.query);
|
|
328
|
+
if ('error' in resolved)
|
|
329
|
+
return (0, exports.fail)(400, resolved.error.code, resolved.error.description);
|
|
330
|
+
const q = resolved.query;
|
|
331
|
+
const client = e.db.oauthClients.get(q.get('client_id') ?? undefined);
|
|
332
|
+
if (!client || client.deleted)
|
|
333
|
+
return (0, exports.fail)(400, 'invalid_request', 'Unknown client_id.');
|
|
334
|
+
const redirectUri = q.get('redirect_uri') ?? (client.redirect_uris.length === 1 ? client.redirect_uris[0] : null);
|
|
335
|
+
if (!redirectUri || !client.redirect_uris.includes(redirectUri))
|
|
336
|
+
return (0, exports.fail)(400, 'invalid_request', 'redirect_uri is not registered for this client.');
|
|
337
|
+
if (client.require_pushed_authorization_requests && !c.query.get('request_uri'))
|
|
338
|
+
return (0, exports.fail)(400, 'invalid_request', 'This client must push its authorization request (POST /oauth2/par) and send the request_uri.');
|
|
339
|
+
const state = q.get('state') ?? undefined;
|
|
340
|
+
const back = (params) => redirect(redirectUri, { ...params, state, iss: e.issuer });
|
|
341
|
+
if (q.get('response_type') !== 'code')
|
|
342
|
+
return back({ error: 'unsupported_response_type', error_description: 'Only response_type=code is supported.' });
|
|
343
|
+
const scope = (0, exports.splitScope)(q.get('scope') ?? 'openid');
|
|
344
|
+
const outside = scope.filter((s) => !client.allowed_scopes.includes(s) && !SCOPES.includes(s));
|
|
345
|
+
if (outside.length)
|
|
346
|
+
return back({ error: 'invalid_scope', error_description: `Scope not allowed: ${outside.join(' ')}.` });
|
|
347
|
+
const challenge = q.get('code_challenge');
|
|
348
|
+
if (challenge && (q.get('code_challenge_method') ?? 'plain') !== 'S256')
|
|
349
|
+
return back({ error: 'invalid_request', error_description: 'code_challenge_method must be S256.' });
|
|
350
|
+
if (!challenge && client.auth_method === 'none')
|
|
351
|
+
return back({ error: 'invalid_request', error_description: 'A public client must use PKCE (code_challenge).' });
|
|
352
|
+
if (q.get('emulator_decision') === 'deny')
|
|
353
|
+
return back({ error: 'access_denied', error_description: 'The user denied the request.' });
|
|
354
|
+
const hint = q.get('login_hint');
|
|
355
|
+
const live = e.db.users.filter((u) => !u.deleted);
|
|
356
|
+
const user = (hint && live.find((u) => u.id === hint || u.emails.some((m) => m.email_address.toLowerCase() === hint.toLowerCase()))) ||
|
|
357
|
+
// A hint that names nobody never falls back to another user.
|
|
358
|
+
(hint ? undefined : (0, exports.st)(e).autoUser ? live.find((u) => u.id === (0, exports.st)(e).autoUser) : undefined) ||
|
|
359
|
+
(!hint && live.length === 1 ? live[0] : undefined);
|
|
360
|
+
if (!user)
|
|
361
|
+
return back({ error: 'login_required', error_description: 'No user to sign in. Pass login_hint or POST /__emulator/oauth/login.' });
|
|
362
|
+
if (user.banned)
|
|
363
|
+
return back({ error: 'access_denied', error_description: 'The user is banned.' });
|
|
364
|
+
const gate = (0, oauth_flows_1.consentGate)(e, { client, user, scope, redirectUri, challenge, nonce: q.get('nonce'), state, prompt: q.get('prompt') });
|
|
365
|
+
if (gate)
|
|
366
|
+
return gate;
|
|
367
|
+
return back({ code: issueCode(e, { client, userId: user.id, scope, redirectUri, challenge, nonce: q.get('nonce') }) });
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
{ method: 'POST', pattern: '/__emulator/oauth/login', public: true, handler: (c) => (((0, exports.st)(c.engine).autoUser = typeof c.body.user_id === 'string' ? c.body.user_id : null), { body: { ok: true } }) },
|
|
371
|
+
// ── token ──
|
|
372
|
+
{
|
|
373
|
+
method: 'POST',
|
|
374
|
+
pattern: '/oauth2/token',
|
|
375
|
+
public: true,
|
|
376
|
+
handler: async (c) => {
|
|
377
|
+
const e = c.engine;
|
|
378
|
+
const auth = authenticateClient(e, c);
|
|
379
|
+
if ('error' in auth)
|
|
380
|
+
return auth.error;
|
|
381
|
+
const client = auth.client;
|
|
382
|
+
const type = String(c.body.grant_type ?? '');
|
|
383
|
+
if (!type)
|
|
384
|
+
return (0, exports.fail)(400, 'invalid_request', 'grant_type is required.');
|
|
385
|
+
if (type !== EXCHANGE && !client.grant_types.includes(type) && !(type === 'refresh_token'))
|
|
386
|
+
return (0, exports.fail)(400, 'unauthorized_client', `This client may not use ${type}.`);
|
|
387
|
+
const s = (0, exports.st)(e);
|
|
388
|
+
const now = e.clock.now();
|
|
389
|
+
const pr = await provedOf(c, client);
|
|
390
|
+
if ('error' in pr)
|
|
391
|
+
return pr.error;
|
|
392
|
+
const cnf = cnfFor(pr.proved);
|
|
393
|
+
const ext = await (0, oauth_flows_1.extensionGrant)(c, client, type, cnf);
|
|
394
|
+
if (ext)
|
|
395
|
+
return ext;
|
|
396
|
+
if (type === 'authorization_code') {
|
|
397
|
+
const rec = s.codes.get(String(c.body.code ?? ''));
|
|
398
|
+
if (!rec)
|
|
399
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The authorization code is invalid.');
|
|
400
|
+
const grant = s.grants.get(rec.grantId);
|
|
401
|
+
if (rec.used) {
|
|
402
|
+
// RFC 6749 section 4.1.2: a replayed code revokes what it already produced.
|
|
403
|
+
grant.revoked = true;
|
|
404
|
+
for (const j of grant.jtis) {
|
|
405
|
+
const a = s.access.get(j);
|
|
406
|
+
if (a)
|
|
407
|
+
a.revoked = true;
|
|
408
|
+
}
|
|
409
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The authorization code was already used.');
|
|
410
|
+
}
|
|
411
|
+
if (grant.clientId !== client.id)
|
|
412
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The code was issued to another client.');
|
|
413
|
+
if (rec.expiresAt <= now)
|
|
414
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The authorization code has expired.');
|
|
415
|
+
if (rec.redirectUri !== (c.body.redirect_uri ?? rec.redirectUri))
|
|
416
|
+
return (0, exports.fail)(400, 'invalid_grant', 'redirect_uri does not match the authorization request.');
|
|
417
|
+
if (rec.challenge) {
|
|
418
|
+
const verifier = String(c.body.code_verifier ?? '');
|
|
419
|
+
if (!verifier || (await (0, crypto_util_1.pkceChallenge)(verifier)) !== rec.challenge)
|
|
420
|
+
return (0, exports.fail)(400, 'invalid_grant', 'PKCE verification failed.');
|
|
421
|
+
}
|
|
422
|
+
rec.used = true;
|
|
423
|
+
if (pr.proved.jkt)
|
|
424
|
+
grant.jkt = pr.proved.jkt;
|
|
425
|
+
return { body: await issueTokens(e, client, grant, rec.nonce, rec.authTime, undefined, cnf), headers: { 'cache-control': 'no-store', pragma: 'no-cache' } };
|
|
426
|
+
}
|
|
427
|
+
if (type === 'refresh_token') {
|
|
428
|
+
const token = String(c.body.refresh_token ?? '');
|
|
429
|
+
const rec = s.refresh.get(token);
|
|
430
|
+
const grant = rec ? s.grants.get(rec.grantId) : undefined;
|
|
431
|
+
if (!rec || !grant || grant.clientId !== client.id)
|
|
432
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The refresh token is invalid.');
|
|
433
|
+
if (grant.revoked)
|
|
434
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The grant was revoked.');
|
|
435
|
+
// A DPoP-bound refresh token is usable only with the key it was bound to (RFC 9449 section 8).
|
|
436
|
+
if (grant.jkt && pr.proved.jkt !== grant.jkt)
|
|
437
|
+
return (0, exports.fail)(400, 'invalid_dpop_proof', 'This refresh token is bound to a DPoP key: send a proof signed with it.');
|
|
438
|
+
if (rec.rotatedAt !== undefined) {
|
|
439
|
+
if (now - rec.rotatedAt <= GRACE_MS && rec.successor)
|
|
440
|
+
return { body: await issueTokens(e, client, grant, null, now, rec.successor, cnf), headers: { 'cache-control': 'no-store' } };
|
|
441
|
+
grant.revoked = true; // reuse: end the whole family
|
|
442
|
+
for (const j of grant.jtis) {
|
|
443
|
+
const a = s.access.get(j);
|
|
444
|
+
if (a)
|
|
445
|
+
a.revoked = true;
|
|
446
|
+
}
|
|
447
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The refresh token was already used; the grant was revoked.');
|
|
448
|
+
}
|
|
449
|
+
const asked = (0, exports.splitScope)(c.body.scope);
|
|
450
|
+
if (asked.some((x) => !grant.scope.includes(x)))
|
|
451
|
+
return (0, exports.fail)(400, 'invalid_scope', 'A refresh cannot widen the granted scope.');
|
|
452
|
+
const narrowed = asked.length ? { ...grant, scope: asked } : grant;
|
|
453
|
+
const next = `ort_${(0, store_1.randomToken)(32)}`;
|
|
454
|
+
rec.rotatedAt = now;
|
|
455
|
+
rec.successor = next;
|
|
456
|
+
s.refresh.set(next, { grantId: grant.id });
|
|
457
|
+
return { body: await issueTokens(e, client, narrowed, null, now, next, cnf), headers: { 'cache-control': 'no-store' } };
|
|
458
|
+
}
|
|
459
|
+
if (type === 'client_credentials') {
|
|
460
|
+
if (client.auth_method === 'none')
|
|
461
|
+
return (0, exports.fail)(401, 'invalid_client', 'A public client cannot use client_credentials.');
|
|
462
|
+
const asked = (0, exports.splitScope)(c.body.scope);
|
|
463
|
+
const scope = asked.length ? asked : client.allowed_scopes.filter((x) => !SCOPES.includes(x));
|
|
464
|
+
if (asked.some((x) => !client.allowed_scopes.includes(x)))
|
|
465
|
+
return (0, exports.fail)(400, 'invalid_scope', 'Scope not allowed for this client.');
|
|
466
|
+
const a = await mintAccess(e, { client, userId: null, scope, grant: null, audience: typeof c.body.audience === 'string' ? c.body.audience : undefined, cnf });
|
|
467
|
+
return { body: { access_token: a.token, token_type: cnf?.jkt ? 'DPoP' : 'Bearer', expires_in: ACCESS_TTL_S, scope: scope.join(' ') }, headers: { 'cache-control': 'no-store' } };
|
|
468
|
+
}
|
|
469
|
+
if (type === EXCHANGE) {
|
|
470
|
+
const subject = String(c.body.subject_token ?? '');
|
|
471
|
+
if (!subject)
|
|
472
|
+
return (0, exports.fail)(400, 'invalid_request', 'subject_token is required.');
|
|
473
|
+
const verified = await e.tokens.verifyAny(subject);
|
|
474
|
+
if (!verified.ok)
|
|
475
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The subject token is not valid.');
|
|
476
|
+
const claims = verified.claims;
|
|
477
|
+
const rec = typeof claims.jti === 'string' ? s.access.get(claims.jti) : undefined;
|
|
478
|
+
if (rec?.revoked || (rec && s.grants.get(rec.grantId ?? '')?.revoked))
|
|
479
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The subject token was revoked.');
|
|
480
|
+
// A session token may be exchanged too: its subject is the user, its scope the client's allowed set.
|
|
481
|
+
const held = typeof claims.scope === 'string' ? (0, exports.splitScope)(claims.scope) : client.allowed_scopes;
|
|
482
|
+
const asked = (0, exports.splitScope)(c.body.scope);
|
|
483
|
+
if (asked.some((x) => !held.includes(x)))
|
|
484
|
+
return (0, exports.fail)(400, 'invalid_scope', 'Token exchange can only narrow scope.');
|
|
485
|
+
let actor;
|
|
486
|
+
if (c.body.actor_token) {
|
|
487
|
+
const a = await e.tokens.verifyAny(String(c.body.actor_token));
|
|
488
|
+
if (!a.ok)
|
|
489
|
+
return (0, exports.fail)(400, 'invalid_grant', 'The actor token is not valid.');
|
|
490
|
+
actor = { sub: a.claims.sub, ...(a.claims.act ? { act: a.claims.act } : {}) };
|
|
491
|
+
}
|
|
492
|
+
const scope = asked.length ? asked : held;
|
|
493
|
+
const out = await mintAccess(e, { client, userId: typeof claims.sub === 'string' && !String(claims.sub).startsWith('oac_') ? claims.sub : null, scope, grant: null, audience: typeof c.body.audience === 'string' ? c.body.audience : undefined, extra: actor ? { act: actor } : undefined, cnf });
|
|
494
|
+
return { body: { access_token: out.token, issued_token_type: ACCESS_TYPE, token_type: cnf?.jkt ? 'DPoP' : 'Bearer', expires_in: ACCESS_TTL_S, scope: scope.join(' ') }, headers: { 'cache-control': 'no-store' } };
|
|
495
|
+
}
|
|
496
|
+
return (0, exports.fail)(400, 'unsupported_grant_type', `Unsupported grant_type ${type}.`);
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
// ── revoke / introspect / userinfo ──
|
|
500
|
+
{
|
|
501
|
+
method: 'POST',
|
|
502
|
+
pattern: '/oauth2/revoke',
|
|
503
|
+
public: true,
|
|
504
|
+
handler: (c) => {
|
|
505
|
+
const e = c.engine;
|
|
506
|
+
const auth = authenticateClient(e, c);
|
|
507
|
+
if ('error' in auth)
|
|
508
|
+
return auth.error;
|
|
509
|
+
const token = String(c.body.token ?? '');
|
|
510
|
+
if (!token)
|
|
511
|
+
return (0, exports.fail)(400, 'invalid_request', 'token is required.');
|
|
512
|
+
const s = (0, exports.st)(e);
|
|
513
|
+
// RFC 7009: always 200, whatever the token was; only the owning client's tokens are touched.
|
|
514
|
+
const rt = s.refresh.get(token);
|
|
515
|
+
const grantOf = rt ? s.grants.get(rt.grantId) : undefined;
|
|
516
|
+
if (grantOf && grantOf.clientId === auth.client.id) {
|
|
517
|
+
grantOf.revoked = true;
|
|
518
|
+
for (const j of grantOf.jtis) {
|
|
519
|
+
const a = s.access.get(j);
|
|
520
|
+
if (a)
|
|
521
|
+
a.revoked = true;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
try {
|
|
526
|
+
const claims = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
|
|
527
|
+
const a = s.access.get(claims.jti);
|
|
528
|
+
if (a && a.clientId === auth.client.id)
|
|
529
|
+
a.revoked = true;
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
/* not a token we issued: nothing to revoke */
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return { status: 200, headers: { 'cache-control': 'no-store' } };
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
method: 'POST',
|
|
540
|
+
pattern: '/oauth2/introspect',
|
|
541
|
+
public: true,
|
|
542
|
+
handler: async (c) => {
|
|
543
|
+
const e = c.engine;
|
|
544
|
+
const auth = authenticateClient(e, c);
|
|
545
|
+
if ('error' in auth)
|
|
546
|
+
return auth.error;
|
|
547
|
+
const token = String(c.body.token ?? '');
|
|
548
|
+
const s = (0, exports.st)(e);
|
|
549
|
+
const inactive = { body: { active: false }, headers: { 'cache-control': 'no-store' } };
|
|
550
|
+
const rt = s.refresh.get(token);
|
|
551
|
+
if (rt) {
|
|
552
|
+
const g = s.grants.get(rt.grantId);
|
|
553
|
+
if (!g || g.revoked || rt.rotatedAt !== undefined || g.clientId !== auth.client.id)
|
|
554
|
+
return inactive;
|
|
555
|
+
return { body: { active: true, token_use: 'refresh_token', client_id: g.clientId, sub: g.userId, scope: g.scope.join(' ') }, headers: { 'cache-control': 'no-store' } };
|
|
556
|
+
}
|
|
557
|
+
const v = await e.tokens.verifyAny(token);
|
|
558
|
+
if (!v.ok || v.claims.token_use !== 'access_token')
|
|
559
|
+
return inactive;
|
|
560
|
+
const rec = s.access.get(String(v.claims.jti));
|
|
561
|
+
if (!rec || rec.revoked || (rec.grantId && s.grants.get(rec.grantId)?.revoked))
|
|
562
|
+
return inactive;
|
|
563
|
+
if (rec.clientId !== auth.client.id)
|
|
564
|
+
return inactive; // another client's token: do not leak
|
|
565
|
+
return { body: { active: true, token_type: rec.cnf?.jkt ? 'DPoP' : 'Bearer', ...(rec.cnf ? { cnf: rec.cnf } : {}), token_use: 'access_token', client_id: rec.clientId, sub: v.claims.sub, aud: v.claims.aud, iss: v.claims.iss, scope: rec.scope.join(' '), exp: v.claims.exp, iat: v.claims.iat, jti: v.claims.jti, ...(v.claims.act ? { act: v.claims.act } : {}) }, headers: { 'cache-control': 'no-store' } };
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
...['GET', 'POST'].map((method) => ({
|
|
569
|
+
method,
|
|
570
|
+
pattern: '/oauth2/userinfo',
|
|
571
|
+
public: true,
|
|
572
|
+
handler: async (c) => {
|
|
573
|
+
const e = c.engine;
|
|
574
|
+
const m = /^(Bearer|DPoP)\s+(.+)$/i.exec(c.request.headers.get('authorization') ?? '');
|
|
575
|
+
const bearer = m?.[2];
|
|
576
|
+
if (!bearer)
|
|
577
|
+
return { status: 401, headers: { 'www-authenticate': 'Bearer error="invalid_token"' }, body: { error: 'invalid_token', error_description: 'A bearer access token is required.' } };
|
|
578
|
+
const v = await e.tokens.verifyAny(bearer);
|
|
579
|
+
const rec = v.ok ? (0, exports.st)(e).access.get(String(v.claims.jti)) : undefined;
|
|
580
|
+
if (!v.ok || v.claims.token_use !== 'access_token' || !rec || rec.revoked || (rec.grantId && (0, exports.st)(e).grants.get(rec.grantId)?.revoked) || !rec.userId) {
|
|
581
|
+
return { status: 401, headers: { 'www-authenticate': 'Bearer error="invalid_token"' }, body: { error: 'invalid_token', error_description: 'The access token is not valid.' } };
|
|
582
|
+
}
|
|
583
|
+
if (rec.cnf?.jkt) {
|
|
584
|
+
// A DPoP-bound token is presented with the DPoP scheme and a proof bound to this request and this token (RFC 9449 section 7).
|
|
585
|
+
const proof = c.request.headers.get('dpop');
|
|
586
|
+
const bad = { status: 401, headers: { 'www-authenticate': 'DPoP error="invalid_dpop_proof", algs="ES256 ES384 RS256 PS256 EdDSA"' }, body: { error: 'invalid_dpop_proof', error_description: 'A DPoP proof bound to this access token is required.' } };
|
|
587
|
+
if (m?.[1]?.toLowerCase() !== 'dpop' || !proof)
|
|
588
|
+
return bad;
|
|
589
|
+
try {
|
|
590
|
+
const r = await (0, jws_1.verifyDpopProof)({ proof, htm: c.request.method, htu: c.request.url, nowMs: e.clock.now(), accessToken: bearer });
|
|
591
|
+
if (r.jkt !== rec.cnf.jkt || !(0, oauth_flows_1.claimDpopJti)(e, r.jkt, r.jti, (jws_1.DPOP_IAT_WINDOW_S + 5) * 1000))
|
|
592
|
+
return bad;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
return bad;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else if (rec.cnf?.['x5t#S256'] && (await (0, jws_1.forwardedCertThumbprint)(c.request.headers)) !== rec.cnf['x5t#S256']) {
|
|
599
|
+
return { status: 401, headers: { 'www-authenticate': 'Bearer error="invalid_token"' }, body: { error: 'invalid_token', error_description: 'The token is bound to a client certificate that was not presented.' } };
|
|
600
|
+
}
|
|
601
|
+
if (!rec.scope.includes('openid'))
|
|
602
|
+
return { status: 403, headers: { 'www-authenticate': 'Bearer error="insufficient_scope", scope="openid"' }, body: { error: 'insufficient_scope', error_description: 'The openid scope is required.' } };
|
|
603
|
+
return { body: { sub: rec.userId, ...userClaims(e, rec.userId, rec.scope) } };
|
|
604
|
+
},
|
|
605
|
+
})),
|
|
606
|
+
];
|
|
607
|
+
}
|
|
608
|
+
//# sourceMappingURL=oauth.js.map
|