@maronn-openid-connect/cli 0.7.1 → 0.8.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.
Files changed (67) hide show
  1. package/README.md +83 -7
  2. package/dist/frameworks/hono/index.d.ts.map +1 -1
  3. package/dist/frameworks/hono/index.js +18 -9
  4. package/dist/frameworks/hono/index.js.map +1 -1
  5. package/dist/frameworks/hono/pages.d.ts +7 -6
  6. package/dist/frameworks/hono/pages.d.ts.map +1 -1
  7. package/dist/frameworks/hono/pages.js +62 -44
  8. package/dist/frameworks/hono/pages.js.map +1 -1
  9. package/dist/frameworks/hono/templates.d.ts +19 -6
  10. package/dist/frameworks/hono/templates.d.ts.map +1 -1
  11. package/dist/frameworks/hono/templates.js +284 -304
  12. package/dist/frameworks/hono/templates.js.map +1 -1
  13. package/dist/frameworks/hono/views.d.ts +3 -0
  14. package/dist/frameworks/hono/views.d.ts.map +1 -0
  15. package/dist/frameworks/hono/views.js +458 -0
  16. package/dist/frameworks/hono/views.js.map +1 -0
  17. package/dist/frameworks/nextjs/authorize.d.ts +3 -0
  18. package/dist/frameworks/nextjs/authorize.d.ts.map +1 -0
  19. package/dist/frameworks/nextjs/authorize.js +757 -0
  20. package/dist/frameworks/nextjs/authorize.js.map +1 -0
  21. package/dist/frameworks/nextjs/ciba.d.ts +8 -0
  22. package/dist/frameworks/nextjs/ciba.d.ts.map +1 -0
  23. package/dist/frameworks/nextjs/ciba.js +591 -0
  24. package/dist/frameworks/nextjs/ciba.js.map +1 -0
  25. package/dist/frameworks/nextjs/conformance.d.ts +3 -0
  26. package/dist/frameworks/nextjs/conformance.d.ts.map +1 -0
  27. package/dist/frameworks/nextjs/conformance.js +2447 -0
  28. package/dist/frameworks/nextjs/conformance.js.map +1 -0
  29. package/dist/frameworks/nextjs/device.d.ts +8 -0
  30. package/dist/frameworks/nextjs/device.d.ts.map +1 -0
  31. package/dist/frameworks/nextjs/device.js +662 -0
  32. package/dist/frameworks/nextjs/device.js.map +1 -0
  33. package/dist/frameworks/nextjs/endpoints.d.ts +7 -0
  34. package/dist/frameworks/nextjs/endpoints.d.ts.map +1 -0
  35. package/dist/frameworks/nextjs/endpoints.js +673 -0
  36. package/dist/frameworks/nextjs/endpoints.js.map +1 -0
  37. package/dist/frameworks/nextjs/index.d.ts.map +1 -1
  38. package/dist/frameworks/nextjs/index.js +135 -2
  39. package/dist/frameworks/nextjs/index.js.map +1 -1
  40. package/dist/frameworks/nextjs/interaction.d.ts +13 -0
  41. package/dist/frameworks/nextjs/interaction.d.ts.map +1 -0
  42. package/dist/frameworks/nextjs/interaction.js +864 -0
  43. package/dist/frameworks/nextjs/interaction.js.map +1 -0
  44. package/dist/frameworks/nextjs/logout.d.ts +5 -0
  45. package/dist/frameworks/nextjs/logout.d.ts.map +1 -0
  46. package/dist/frameworks/nextjs/logout.js +271 -0
  47. package/dist/frameworks/nextjs/logout.js.map +1 -0
  48. package/dist/frameworks/nextjs/metadata.d.ts +6 -0
  49. package/dist/frameworks/nextjs/metadata.d.ts.map +1 -0
  50. package/dist/frameworks/nextjs/metadata.js +339 -0
  51. package/dist/frameworks/nextjs/metadata.js.map +1 -0
  52. package/dist/frameworks/nextjs/provider.d.ts +6 -0
  53. package/dist/frameworks/nextjs/provider.d.ts.map +1 -0
  54. package/dist/frameworks/nextjs/provider.js +797 -0
  55. package/dist/frameworks/nextjs/provider.js.map +1 -0
  56. package/dist/frameworks/nextjs/token.d.ts +7 -0
  57. package/dist/frameworks/nextjs/token.d.ts.map +1 -0
  58. package/dist/frameworks/nextjs/token.js +1337 -0
  59. package/dist/frameworks/nextjs/token.js.map +1 -0
  60. package/dist/frameworks/web-standard/templates.d.ts +1 -13
  61. package/dist/frameworks/web-standard/templates.d.ts.map +1 -1
  62. package/dist/frameworks/web-standard/templates.js +12 -1296
  63. package/dist/frameworks/web-standard/templates.js.map +1 -1
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +38 -8
  66. package/dist/index.js.map +1 -1
  67. package/package.json +1 -1
@@ -0,0 +1,673 @@
1
+ import { DEFAULT_FEATURES } from '../../features.js';
2
+ import { EXPERIMENTAL_PACKAGE } from '../hono/templates.js';
3
+ export function nextJsUserinfoRouteTemplate(corePkg) {
4
+ return `/**
5
+ * UserInfo Endpoint (OIDC Core 1.0 §5.3).
6
+ *
7
+ * The response format follows the client metadata userinfo_signed_response_alg:
8
+ * a signed JWT (application/jwt, §5.3.2) when the client registered one,
9
+ * JSON otherwise. Every answer — success and error — exposes or concerns PII,
10
+ * so none may be cached (RFC 6750 §5.2 / OIDC Core 1.0 §16.4).
11
+ */
12
+ import {
13
+ resolveUserInfoAccessToken,
14
+ validateUserInfoTokenExpiration,
15
+ validateUserInfoScope,
16
+ validateUserInfoAudience,
17
+ resolveUserInfoClaims,
18
+ filterClaimsByScope,
19
+ applyRequestedClaims,
20
+ generateUserInfoJwt,
21
+ selectSigningKeyByAlg,
22
+ UserInfoError,
23
+ type SigningKey,
24
+ type UserInfoResponse,
25
+ } from '${corePkg}';
26
+ import type { RegisteredClient } from '../_oidc-provider/config';
27
+ import {
28
+ clientResolver,
29
+ config,
30
+ loadSigningKeys,
31
+ resolvers,
32
+ type ProviderSigningKeys,
33
+ } from '../_oidc-provider/provider';
34
+ import {
35
+ clientCors,
36
+ corsPreflight,
37
+ noStoreJson,
38
+ signingKeysUnavailable,
39
+ withCors,
40
+ } from '../_oidc-provider/http';
41
+
42
+ export const dynamic = 'force-dynamic';
43
+ export const runtime = 'nodejs';
44
+
45
+ export async function GET(request: Request): Promise<Response> {
46
+ return withCors(request, clientCors, await userinfo(request));
47
+ }
48
+
49
+ export async function POST(request: Request): Promise<Response> {
50
+ return withCors(request, clientCors, await userinfo(request));
51
+ }
52
+
53
+ export function OPTIONS(request: Request): Response {
54
+ return corsPreflight(request, clientCors);
55
+ }
56
+
57
+ async function userinfo(request: Request): Promise<Response> {
58
+ const keys = await loadSigningKeys().catch(() => null);
59
+ if (!keys) return signingKeysUnavailable();
60
+
61
+ let accessToken: string;
62
+ try {
63
+ const { token, methodCount } = await extractAccessToken(request);
64
+ if (methodCount > 1) {
65
+ // RFC 6750 §2: clients MUST NOT use more than one method per request.
66
+ return noStoreJson(
67
+ {
68
+ error: 'invalid_request',
69
+ error_description: 'Multiple access token methods are not allowed',
70
+ },
71
+ {
72
+ status: 400,
73
+ headers: { 'WWW-Authenticate': 'Bearer realm="UserInfo", error="invalid_request"' },
74
+ },
75
+ );
76
+ }
77
+ if (!token) {
78
+ // RFC 6750 §3.1: a request without authentication information gets a
79
+ // challenge that only names the realm (no error / error_description).
80
+ return noStoreJson(
81
+ { error: 'invalid_token', error_description: 'Access token is required' },
82
+ { status: 401, headers: { 'WWW-Authenticate': 'Bearer realm="UserInfo"' } },
83
+ );
84
+ }
85
+ accessToken = token;
86
+ } catch {
87
+ return noStoreJson({ error: 'invalid_request' }, { status: 400 });
88
+ }
89
+
90
+ try {
91
+ // --- UserInfo request pipeline ------------------------------------------
92
+ // Each step below is an independent core function, called in the same order
93
+ // as core's handleUserInfoRequest(). Delete a call to drop that validation,
94
+ // or insert your own logic between steps. Every step throws UserInfoError,
95
+ // which the catch block below renders as an RFC 6750 Bearer challenge.
96
+
97
+ // OIDC Core 1.0 §5.3.1: resolve the presented Bearer token (invalid_token when unknown).
98
+ const tokenInfo = await resolveUserInfoAccessToken(accessToken, resolvers.accessTokenResolver);
99
+
100
+ // RFC 6750 §3.1: an expired access token is invalid_token.
101
+ validateUserInfoTokenExpiration(tokenInfo);
102
+
103
+ // OIDC Core 1.0 §5.3.1: the token must carry the openid scope (insufficient_scope).
104
+ validateUserInfoScope(tokenInfo);
105
+
106
+ // RFC 9068 §4: this UserInfo endpoint must appear in the access token's aud.
107
+ // The token endpoint always puts it there (buildAccessTokenAudience), so the
108
+ // check is on for JWT and opaque tokens alike. Pass undefined to turn it off.
109
+ validateUserInfoAudience(tokenInfo, \`\${config.issuer}/userinfo\`);
110
+
111
+ // Load every claim the OP knows about the token's subject.
112
+ const userClaims = await resolveUserInfoClaims(tokenInfo, resolvers.userClaimsResolver);
113
+
114
+ // OIDC Core 1.0 §5.4: keep only the claims the granted scopes allow.
115
+ const scopedResponse = filterClaimsByScope(userClaims, tokenInfo.scope);
116
+
117
+ // OIDC Core 1.0 §5.5: overlay the individually requested claims the token
118
+ // endpoint stored with this access token (claims.userinfo members).
119
+ const response = applyRequestedClaims(scopedResponse, userClaims, tokenInfo.claims);
120
+
121
+ const client = (await clientResolver.findClient(tokenInfo.clientId)) as RegisteredClient | null;
122
+ if (client?.userinfoSignedResponseAlg) {
123
+ return await signedUserInfo(response, client, keys);
124
+ }
125
+ return noStoreJson(response);
126
+ } catch (error) {
127
+ if (error instanceof UserInfoError) {
128
+ return noStoreJson(
129
+ { error: error.error, error_description: error.errorDescription },
130
+ {
131
+ status: error.statusCode,
132
+ headers: {
133
+ 'WWW-Authenticate': \`Bearer realm="UserInfo", error="\${error.error}", error_description="\${error.errorDescription}"\`,
134
+ },
135
+ },
136
+ );
137
+ }
138
+ return noStoreJson({ error: 'server_error' }, { status: 500 });
139
+ }
140
+ }
141
+
142
+ /**
143
+ * OIDC Core 1.0 §5.3.2: the UserInfo Response as a JWS signed with the alg the
144
+ * client registered (RS256, ES256, ...), using the registered UserInfo key of
145
+ * that alg. No key for it is a server configuration error — the response is
146
+ * never silently signed with another alg.
147
+ */
148
+ async function signedUserInfo(
149
+ claims: UserInfoResponse,
150
+ client: RegisteredClient,
151
+ keys: ProviderSigningKeys,
152
+ ): Promise<Response> {
153
+ let signingKey: SigningKey;
154
+ try {
155
+ signingKey = selectSigningKeyByAlg(keys.userinfo.registered, client.userinfoSignedResponseAlg);
156
+ } catch {
157
+ return noStoreJson(
158
+ {
159
+ error: 'server_error',
160
+ error_description: \`No UserInfo signing key registered for alg "\${client.userinfoSignedResponseAlg}"\`,
161
+ },
162
+ { status: 500 },
163
+ );
164
+ }
165
+ const jwt = await generateUserInfoJwt(claims, {
166
+ issuer: config.issuer,
167
+ audience: client.clientId,
168
+ privateKey: signingKey.privateKey,
169
+ keyId: signingKey.keyId,
170
+ });
171
+ return new Response(jwt, {
172
+ headers: { 'Content-Type': 'application/jwt', 'Cache-Control': 'no-store', Pragma: 'no-cache' },
173
+ });
174
+ }
175
+
176
+ /**
177
+ * The access token of the request, from:
178
+ * - the Authorization: Bearer header (RFC 6750 §2.1, REQUIRED), or
179
+ * - the access_token form parameter of a POST (RFC 6750 §2.2, OPTIONAL).
180
+ * The query parameter method (§2.3) is not supported: OAuth 2.1 prohibits it.
181
+ * methodCount lets the caller refuse a request that used more than one.
182
+ */
183
+ async function extractAccessToken(request: Request): Promise<{ token: string; methodCount: number }> {
184
+ const authHeader = request.headers.get('Authorization') ?? '';
185
+ // RFC 7235 §2.1: the auth scheme is case-insensitive; the token value is kept verbatim.
186
+ const spaceIndex = authHeader.indexOf(' ');
187
+ const headerToken =
188
+ spaceIndex !== -1 && authHeader.slice(0, spaceIndex).toLowerCase() === 'bearer'
189
+ ? authHeader.slice(spaceIndex + 1)
190
+ : '';
191
+
192
+ let bodyToken = '';
193
+ if (request.method === 'POST') {
194
+ const mediaType =
195
+ (request.headers.get('Content-Type') ?? '').toLowerCase().split(';')[0]?.trim() ?? '';
196
+ if (mediaType === 'application/x-www-form-urlencoded') {
197
+ bodyToken = new URLSearchParams(await request.text()).get('access_token') ?? '';
198
+ }
199
+ }
200
+
201
+ const methodCount = (headerToken ? 1 : 0) + (bodyToken ? 1 : 0);
202
+ return { token: headerToken || bodyToken, methodCount };
203
+ }
204
+ `;
205
+ }
206
+ export function nextJsIntrospectionRouteTemplate(corePkg, features = DEFAULT_FEATURES) {
207
+ const jwt = features.jwtIntrospectionResponse;
208
+ const jwtCoreImports = jwt
209
+ ? `
210
+ selectSigningKeyByAlg,`
211
+ : '';
212
+ const jwtImports = jwt
213
+ ? `
214
+ import {
215
+ TOKEN_INTROSPECTION_JWT_MEDIA_TYPE,
216
+ acceptsIntrospectionJwt,
217
+ createIntrospectionResponseJwt,
218
+ restrictIntrospectionResponseToCaller,
219
+ } from '${EXPERIMENTAL_PACKAGE}/jwt-introspection-response';`
220
+ : '';
221
+ const keyLoading = jwt
222
+ ? ` const keys = await loadSigningKeys().catch(() => null);
223
+ if (!keys) return signingKeysUnavailable();
224
+
225
+ `
226
+ : '';
227
+ const providerImports = jwt
228
+ ? `import { clientResolver, config, loadSigningKeys, resolvers } from '../_oidc-provider/provider';`
229
+ : `import { clientResolver, resolvers } from '../_oidc-provider/provider';`;
230
+ const httpImports = jwt
231
+ ? `import {
232
+ clientCors,
233
+ corsPreflight,
234
+ isFormUrlEncoded,
235
+ noStoreJson,
236
+ oauthError,
237
+ signingKeysUnavailable,
238
+ withCors,
239
+ } from '../_oidc-provider/http';`
240
+ : `import {
241
+ clientCors,
242
+ corsPreflight,
243
+ isFormUrlEncoded,
244
+ noStoreJson,
245
+ oauthError,
246
+ withCors,
247
+ } from '../_oidc-provider/http';`;
248
+ const jwtBranch = jwt
249
+ ? ` // EXPERIMENTAL — RFC 9701 §4 / §5: a caller whose Accept header names
250
+ // application/token-introspection+jwt receives the response as a signed JWT.
251
+ // Any other request (no Accept, application/json, a wildcard) is answered as
252
+ // JSON, and the branch sits AFTER client authentication and token resolution
253
+ // so the Accept header can never bypass either (§8.2 downgrade prevention).
254
+ if (acceptsIntrospectionJwt(request.headers.get('Accept') ?? undefined)) {
255
+ // RFC 9701 §3 / §5: before the response leaves as a signed assertion its
256
+ // members are restricted to what the authenticated caller may see — a
257
+ // caller that is neither the client the token was issued to nor in its
258
+ // aud gets { active: false }, indistinguishable from an unknown token.
259
+ const restrictedResponse = restrictIntrospectionResponseToCaller(
260
+ response,
261
+ authenticatedClientId,
262
+ );
263
+ // RFC 9701 §6: alg is pinned to RS256 (the default for a client that
264
+ // registered no introspection_signed_response_alg). The active key is not
265
+ // guaranteed to be RS256, so the key is picked by alg from the registered
266
+ // set; its public half is published at /.well-known/jwks.json under the
267
+ // same kid. No RS256 key surfaces as a server_error (a configuration
268
+ // mistake) rather than as an unverifiable response.
269
+ const responseJwt = await createIntrospectionResponseJwt({
270
+ issuer: config.issuer,
271
+ audience: authenticatedClientId,
272
+ introspection: restrictedResponse,
273
+ signingKey: selectSigningKeyByAlg(keys.general.registered, 'RS256'),
274
+ });
275
+ // RFC 9701 §5: the success response is the compact JWS itself, under its
276
+ // own media type.
277
+ return new Response(responseJwt, {
278
+ headers: {
279
+ 'Content-Type': TOKEN_INTROSPECTION_JWT_MEDIA_TYPE,
280
+ 'Cache-Control': 'no-store',
281
+ Pragma: 'no-cache',
282
+ },
283
+ });
284
+ }
285
+
286
+ `
287
+ : '';
288
+ return `/**
289
+ * Token Introspection Endpoint (RFC 7662 §2).
290
+ *
291
+ * Confidential clients only: a caller registered with token_endpoint_auth_method
292
+ * 'none' is rejected with invalid_client (RFC 7662 §2.1 / RFC 9701 §5), since a
293
+ * public client_id alone is not authentication. Responses are never cached
294
+ * (RFC 7662 §2.2).
295
+ */
296
+ import {
297
+ extractClientCredentials,
298
+ resolveAuthenticatedTokenClient,
299
+ validateClientAuthMethod,
300
+ verifyClientSecret,
301
+ requireIntrospectionToken,
302
+ requireIntrospectionClient,
303
+ requireConfidentialIntrospectionCaller,
304
+ resolveIntrospectionToken,
305
+ isIntrospectionTokenActive,
306
+ buildIntrospectionResponse,
307
+ INACTIVE_INTROSPECTION_RESPONSE,
308
+ IntrospectionError,
309
+ TokenError,${jwtCoreImports}
310
+ type IntrospectionResponse,
311
+ } from '${corePkg}';${jwtImports}
312
+ ${providerImports}
313
+ ${httpImports}
314
+
315
+ export const dynamic = 'force-dynamic';
316
+ export const runtime = 'nodejs';
317
+
318
+ export async function POST(request: Request): Promise<Response> {
319
+ return withCors(request, clientCors, await introspect(request));
320
+ }
321
+
322
+ export function OPTIONS(request: Request): Response {
323
+ return corsPreflight(request, clientCors);
324
+ }
325
+
326
+ async function introspect(request: Request): Promise<Response> {
327
+ ${keyLoading} if (!isFormUrlEncoded(request)) {
328
+ return oauthError('invalid_request', 'Content-Type must be application/x-www-form-urlencoded');
329
+ }
330
+ const params = Object.fromEntries(new URLSearchParams(await request.text()));
331
+
332
+ try {
333
+ // --- Client authentication pipeline -------------------------------------
334
+ // OAuth 2.1 §2.3 / OIDC Core 1.0 §9, called in the same order as core's
335
+ // authenticateClient(). RFC 7662 §2.1 requires the caller to authenticate.
336
+ const presentedCredentials = extractClientCredentials({
337
+ params,
338
+ authorizationHeader: request.headers.get('Authorization') ?? '',
339
+ });
340
+ const introspectingClient = await resolveAuthenticatedTokenClient(
341
+ presentedCredentials.clientId,
342
+ clientResolver,
343
+ );
344
+ validateClientAuthMethod(introspectingClient, presentedCredentials);
345
+ await verifyClientSecret(introspectingClient, presentedCredentials.clientSecret);
346
+ // RFC 7662 §2.1 / RFC 9701 §5: the caller must be an authenticated
347
+ // confidential client. A client registered with token_endpoint_auth_method
348
+ // 'none' passes the pipeline above with its client_id alone — public
349
+ // information — so treating it as authenticated would let anyone scan
350
+ // tokens. Revocation has no such step on purpose: RFC 7009 §2.1 lets a
351
+ // public client revoke its own tokens.
352
+ requireConfidentialIntrospectionCaller(introspectingClient);
353
+ const authenticatedClientId = presentedCredentials.clientId;
354
+
355
+ // --- Introspection pipeline ---------------------------------------------
356
+ // Each step below is an independent core function, called in the same order
357
+ // as core's handleIntrospectionRequest(). Delete a call to drop that step,
358
+ // or insert your own logic between steps.
359
+
360
+ // RFC 7662 §2.1: token is REQUIRED (invalid_request when absent).
361
+ const token = requireIntrospectionToken({ token: params.token });
362
+
363
+ // RFC 7662 §2.1: the caller must be an authenticated client (invalid_client).
364
+ requireIntrospectionClient(authenticatedClientId);
365
+
366
+ // RFC 7662 §2.1: token_type_hint only reorders the lookup — the other token
367
+ // type is still searched when the hint misses.
368
+ const resolved = await resolveIntrospectionToken({
369
+ token,
370
+ tokenTypeHint: params.token_type_hint,
371
+ accessTokenResolver: resolvers.introspectionAccessTokenResolver,
372
+ refreshTokenResolver: resolvers.introspectionRefreshTokenResolver,
373
+ });
374
+
375
+ // RFC 7662 §2.2: an unknown, expired, not-yet-valid or rotated token is
376
+ // reported as { active: false } and nothing else, so the caller cannot tell
377
+ // "never existed" from "no longer valid".
378
+ let response: IntrospectionResponse = INACTIVE_INTROSPECTION_RESPONSE;
379
+ if (resolved !== null && isIntrospectionTokenActive(resolved)) {
380
+ response = buildIntrospectionResponse(resolved);
381
+ }
382
+
383
+ ${jwtBranch} return noStoreJson(response);
384
+ } catch (error) {
385
+ if (error instanceof TokenError || error instanceof IntrospectionError) {
386
+ return oauthError(
387
+ error.error,
388
+ error.errorDescription,
389
+ error.statusCode,
390
+ error.wwwAuthenticate ? { 'WWW-Authenticate': error.wwwAuthenticate } : undefined,
391
+ );
392
+ }
393
+ return oauthError('server_error', undefined, 500);
394
+ }
395
+ }
396
+ `;
397
+ }
398
+ export function nextJsRevocationRouteTemplate(corePkg) {
399
+ return `/**
400
+ * Token Revocation Endpoint (RFC 7009 §2).
401
+ *
402
+ * Confidential clients authenticate with their registered method; public
403
+ * clients registered with token_endpoint_auth_method=none identify themselves
404
+ * with client_id alone (RFC 7009 §2.1). "Revoked" and "not found" both answer
405
+ * 200 with no body, so the endpoint reveals nothing about which tokens exist
406
+ * (§2.2). Revoking a refresh token also revokes the access tokens of the same
407
+ * grant (§2.1 SHOULD).
408
+ */
409
+ import {
410
+ extractClientCredentials,
411
+ resolveAuthenticatedTokenClient,
412
+ validateClientAuthMethod,
413
+ verifyClientSecret,
414
+ requireRevocationToken,
415
+ requireRevocationClient,
416
+ resolveRevocationTarget,
417
+ validateRevocationTokenClient,
418
+ revokeResolvedToken,
419
+ revokeGrantAccessTokens,
420
+ RevocationError,
421
+ TokenError,
422
+ } from '${corePkg}';
423
+ import { clientResolver, resolvers } from '../_oidc-provider/provider';
424
+ import {
425
+ clientCors,
426
+ corsPreflight,
427
+ isFormUrlEncoded,
428
+ oauthError,
429
+ withCors,
430
+ } from '../_oidc-provider/http';
431
+
432
+ export const dynamic = 'force-dynamic';
433
+ export const runtime = 'nodejs';
434
+
435
+ export async function POST(request: Request): Promise<Response> {
436
+ return withCors(request, clientCors, await revoke(request));
437
+ }
438
+
439
+ export function OPTIONS(request: Request): Response {
440
+ return corsPreflight(request, clientCors);
441
+ }
442
+
443
+ async function revoke(request: Request): Promise<Response> {
444
+ if (!isFormUrlEncoded(request)) {
445
+ return oauthError('invalid_request', 'Content-Type must be application/x-www-form-urlencoded');
446
+ }
447
+ const params = Object.fromEntries(new URLSearchParams(await request.text()));
448
+ const { revocationResolvers } = resolvers;
449
+
450
+ try {
451
+ // --- Client authentication pipeline -------------------------------------
452
+ // OAuth 2.1 §2.3 / OIDC Core 1.0 §9, called in the same order as core's
453
+ // authenticateClient(). Public clients registered with
454
+ // token_endpoint_auth_method=none pass with client_id only (RFC 7009 §2.1).
455
+ const presentedCredentials = extractClientCredentials({
456
+ params,
457
+ authorizationHeader: request.headers.get('Authorization') ?? '',
458
+ });
459
+ const revokingClient = await resolveAuthenticatedTokenClient(
460
+ presentedCredentials.clientId,
461
+ clientResolver,
462
+ );
463
+ validateClientAuthMethod(revokingClient, presentedCredentials);
464
+ await verifyClientSecret(revokingClient, presentedCredentials.clientSecret);
465
+ const authenticatedClientId = presentedCredentials.clientId;
466
+
467
+ // --- Revocation pipeline ------------------------------------------------
468
+ // Each step below is an independent core function, called in the same order
469
+ // as core's handleRevocationRequest(). Delete a call to drop that step,
470
+ // or insert your own logic between steps.
471
+
472
+ // RFC 7009 §2.1: token is REQUIRED (invalid_request when absent).
473
+ const token = requireRevocationToken({ token: params.token });
474
+
475
+ // RFC 7009 §2.1: the caller must be an identified client (invalid_client).
476
+ requireRevocationClient(authenticatedClientId);
477
+
478
+ // RFC 7009 §2.1: token_type_hint only reorders the lookup — the other token
479
+ // type is still searched when the hint misses.
480
+ const resolved = await resolveRevocationTarget({
481
+ token,
482
+ tokenTypeHint: params.token_type_hint,
483
+ resolvers: revocationResolvers,
484
+ });
485
+
486
+ // RFC 7009 §2.2: an unknown token is still a success, so the client cannot
487
+ // probe which token values exist.
488
+ if (resolved !== null) {
489
+ // RFC 7009 §2.1: a token issued to another client is refused (invalid_grant).
490
+ validateRevocationTokenClient(resolved, authenticatedClientId);
491
+
492
+ await revokeResolvedToken(token, resolved, revocationResolvers);
493
+
494
+ // RFC 7009 §2.1 SHOULD: revoking a refresh token also revokes the access
495
+ // tokens of the same grant. Delete this call to revoke only the presented
496
+ // token.
497
+ await revokeGrantAccessTokens(resolved, revocationResolvers);
498
+ }
499
+
500
+ // RFC 7009 §2.2: 200 with an empty body.
501
+ return new Response(null, {
502
+ status: 200,
503
+ headers: { 'Cache-Control': 'no-store', Pragma: 'no-cache' },
504
+ });
505
+ } catch (error) {
506
+ if (error instanceof TokenError || error instanceof RevocationError) {
507
+ return oauthError(
508
+ error.error,
509
+ error.errorDescription,
510
+ error.statusCode,
511
+ error.wwwAuthenticate ? { 'WWW-Authenticate': error.wwwAuthenticate } : undefined,
512
+ );
513
+ }
514
+ return oauthError('server_error', undefined, 500);
515
+ }
516
+ }
517
+ `;
518
+ }
519
+ export function nextJsParConfigTemplate() {
520
+ return `/**
521
+ * EXPERIMENTAL — Pushed Authorization Requests settings (RFC 9126).
522
+ *
523
+ * Read by the PAR endpoint (par/route.ts), the authorization endpoint and
524
+ * discovery, so it lives in its own module.
525
+ *
526
+ * - expiresInSeconds: request_uri lifetime. RFC 9126 §2.2 recommends 5–600
527
+ * seconds; values outside that range fail fast at module load.
528
+ * - requirePushedAuthorizationRequests: RFC 9126 §5. When true, /authorize
529
+ * rejects any request that did not go through /par, and discovery
530
+ * advertises require_pushed_authorization_requests: true.
531
+ */
532
+ import { assertParExpiresInSeconds } from '${EXPERIMENTAL_PACKAGE}/par';
533
+
534
+ export const parConfig = {
535
+ expiresInSeconds: 60,
536
+ requirePushedAuthorizationRequests: false,
537
+ };
538
+
539
+ assertParExpiresInSeconds(parConfig.expiresInSeconds);
540
+ `;
541
+ }
542
+ export function nextJsParRouteTemplate(corePkg) {
543
+ return `/**
544
+ * EXPERIMENTAL — Pushed Authorization Request Endpoint (RFC 9126 §2).
545
+ *
546
+ * Generated because the OP was created with \`--enable par\`. Backed by
547
+ * ${EXPERIMENTAL_PACKAGE}, whose API is NOT stable: it may change in a breaking
548
+ * way between releases. Do not build production code on it without pinning the
549
+ * version.
550
+ *
551
+ * The client POSTs the authorization request parameters here (back channel,
552
+ * authenticated) and receives a short-lived request_uri that it then passes to
553
+ * /authorize.
554
+ *
555
+ * NOTE (RFC 9126 §2.3): request size limits (413) and rate limiting (429) are
556
+ * left to the deployment layer (reverse proxy / platform). This endpoint is
557
+ * unauthenticated until the client credentials are checked, so put a rate
558
+ * limit in front of it in production.
559
+ */
560
+ import {
561
+ ParError,
562
+ authenticateParClient,
563
+ buildPushedAuthorizationResponse,
564
+ createPushedAuthorizationRecord,
565
+ rejectForbiddenParParams,
566
+ validatePushedAuthorizationParams,
567
+ } from '${EXPERIMENTAL_PACKAGE}/par';
568
+ import { sanitizeErrorDescription } from '${corePkg}';
569
+ import { clientResolver, config, parStore } from '../_oidc-provider/provider';
570
+ import {
571
+ clientCors,
572
+ corsPreflight,
573
+ isFormUrlEncoded,
574
+ uniqueParams,
575
+ withCors,
576
+ } from '../_oidc-provider/http';
577
+ import { parConfig } from './config';
578
+
579
+ export const dynamic = 'force-dynamic';
580
+ export const runtime = 'nodejs';
581
+
582
+ export async function POST(request: Request): Promise<Response> {
583
+ return withCors(request, clientCors, await pushAuthorizationRequest(request));
584
+ }
585
+
586
+ export function OPTIONS(request: Request): Response {
587
+ return corsPreflight(request, clientCors);
588
+ }
589
+
590
+ async function pushAuthorizationRequest(request: Request): Promise<Response> {
591
+ // RFC 9126 §2.1: the body MUST be application/x-www-form-urlencoded.
592
+ if (!isFormUrlEncoded(request)) {
593
+ return parJson(
594
+ { error: 'invalid_request', error_description: 'Pushed authorization requests must use application/x-www-form-urlencoded' },
595
+ 400,
596
+ );
597
+ }
598
+
599
+ // RFC 6749 §3.1: request parameters MUST NOT be repeated.
600
+ const { params, duplicateKey } = uniqueParams(new URLSearchParams(await request.text()));
601
+ if (duplicateKey !== undefined) {
602
+ return parJson(
603
+ { error: 'invalid_request', error_description: \`Parameter "\${sanitizeErrorDescription(duplicateKey)}" must not be repeated\` },
604
+ 400,
605
+ );
606
+ }
607
+
608
+ try {
609
+ // --- Pushed authorization request pipeline ------------------------------
610
+ // Each step below is an independent function from
611
+ // ${EXPERIMENTAL_PACKAGE}/par, called in RFC 9126 §2.1 order. Delete a call
612
+ // to drop that validation, or insert your own logic between steps.
613
+
614
+ // RFC 9126 §2.1: request_uri MUST NOT be pushed. The request parameter
615
+ // (PAR + JAR, §3) is not supported by this generated provider.
616
+ rejectForbiddenParParams(params);
617
+
618
+ // RFC 9126 §2.1: authenticate exactly like the token endpoint does. Public
619
+ // clients present only client_id (no credentials).
620
+ const clientId = await authenticateParClient({
621
+ params,
622
+ authorizationHeader: request.headers.get('Authorization') ?? '',
623
+ clientResolver,
624
+ });
625
+
626
+ // client_id is a required authorization request parameter (RFC 9126 §2.1),
627
+ // so it is pinned to the authenticated client before validation and storage.
628
+ const pushedParams = { ...params, client_id: clientId };
629
+
630
+ // RFC 9126 §2.1: "validate the request the same way the authorization
631
+ // endpoint would" — an unregistered redirect_uri or a bad scope fails here,
632
+ // before the End-User ever sees a screen.
633
+ await validatePushedAuthorizationParams(pushedParams, clientResolver, {
634
+ allowNonPkceAuthorizationCodeFlow: config.allowNonPkceAuthorizationCodeFlow,
635
+ });
636
+
637
+ // RFC 9126 §2.2 / §7.1: mint a random reference and store the request under
638
+ // it. Client credentials are never persisted.
639
+ const record = await createPushedAuthorizationRecord({
640
+ clientId,
641
+ params: pushedParams,
642
+ store: parStore,
643
+ expiresInSeconds: parConfig.expiresInSeconds,
644
+ });
645
+ const response = buildPushedAuthorizationResponse(record);
646
+
647
+ // Never log the pushed parameters: they can carry PII such as login_hint,
648
+ // and the Authorization header carries the client_secret.
649
+
650
+ // RFC 9126 §2.2: 201 Created with a non-cacheable JSON body.
651
+ return parJson({ request_uri: response.requestUri, expires_in: response.expiresIn }, 201);
652
+ } catch (error) {
653
+ if (error instanceof ParError) {
654
+ // RFC 9126 §2.3: token-endpoint style JSON errors; this endpoint never redirects.
655
+ return parJson(
656
+ { error: error.code, error_description: error.errorDescription },
657
+ error.statusCode,
658
+ error.wwwAuthenticate,
659
+ );
660
+ }
661
+ return parJson({ error: 'server_error' }, 500);
662
+ }
663
+ }
664
+
665
+ /** RFC 9126 §2.2 / §2.3: every PAR response is JSON that must not be cached. */
666
+ function parJson(body: unknown, status: number, wwwAuthenticate?: string): Response {
667
+ const headers = new Headers({ 'Cache-Control': 'no-cache, no-store', Pragma: 'no-cache' });
668
+ if (wwwAuthenticate) headers.set('WWW-Authenticate', wwwAuthenticate);
669
+ return Response.json(body, { status, headers });
670
+ }
671
+ `;
672
+ }
673
+ //# sourceMappingURL=endpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.js","sourceRoot":"","sources":["../../../src/frameworks/nextjs/endpoints.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAG5D,MAAM,UAAU,2BAA2B,CAAC,OAAe;IACzD,OAAO;;;;;;;;;;;;;;;;;;;;;UAqBC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmLhB,CAAC;AACF,CAAC;AAGD,MAAM,UAAU,gCAAgC,CAC9C,OAAe,EACf,QAAQ,GAAsB,gBAAgB;IAE9C,MAAM,GAAG,GAAG,QAAQ,CAAC,wBAAwB,CAAC;IAC9C,MAAM,cAAc,GAAG,GAAG;QACxB,CAAC,CAAC;yBACmB;QACrB,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,UAAU,GAAG,GAAG;QACpB,CAAC,CAAC;;;;;;UAMI,oBAAoB,+BAA+B;QACzD,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,UAAU,GAAG,GAAG;QACpB,CAAC,CAAC;;;CAGL;QACG,CAAC,CAAC,EAAE,CAAC;IACP,MAAM,eAAe,GAAG,GAAG;QACzB,CAAC,CAAC,kGAAkG;QACpG,CAAC,CAAC,yEAAyE,CAAC;IAC9E,MAAM,WAAW,GAAG,GAAG;QACrB,CAAC,CAAC;;;;;;;;iCAQ2B;QAC7B,CAAC,CAAC;;;;;;;iCAO2B,CAAC;IAChC,MAAM,SAAS,GAAG,GAAG;QACnB,CAAC,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCL;QACG,CAAC,CAAC,EAAE,CAAC;IACP,OAAO;;;;;;;;;;;;;;;;;;;;;eAqBM,cAAc;;UAEnB,OAAO,KAAK,UAAU;EAC9B,eAAe;EACf,WAAW;;;;;;;;;;;;;;EAcX,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwDV,SAAS;;;;;;;;;;;;;CAaV,CAAC;AACF,CAAC;AAGD,MAAM,UAAU,6BAA6B,CAAC,OAAe;IAC3D,OAAO;;;;;;;;;;;;;;;;;;;;;;;UAuBC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+FhB,CAAC;AACF,CAAC;AAGD,MAAM,UAAU,uBAAuB;IACrC,OAAO;;;;;;;;;;;;6CAYoC,oBAAoB;;;;;;;;CAQhE,CAAC;AACF,CAAC;AAGD,MAAM,UAAU,sBAAsB,CAAC,OAAe;IACpD,OAAO;;;;KAIJ,oBAAoB;;;;;;;;;;;;;;;;;;;;UAoBf,oBAAoB;4CACc,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;SA2C1C,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4D5B,CAAC;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/frameworks/nextjs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAGvF,qBAAa,eAAgB,YAAW,kBAAkB;IACxD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,WAAW,aAAa;IAEjC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,EAAE,CAEnD;CACF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/frameworks/nextjs/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA+EvF,qBAAa,eAAgB,YAAW,kBAAkB;IACxD,QAAQ,CAAC,IAAI,YAAY;IACzB,QAAQ,CAAC,WAAW,aAAa;IAEjC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,GAAG,aAAa,EAAE,CAKnD;CACF"}