@maronn-openid-connect/cli 0.7.0 → 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 +439 -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,757 @@
1
+ import { DEFAULT_FEATURES } from '../../features.js';
2
+ import { EXPERIMENTAL_PACKAGE } from '../hono/templates.js';
3
+ export function nextJsAuthorizeRouteTemplate(corePkg, features = DEFAULT_FEATURES, scopes = []) {
4
+ const jarm = features.jarm;
5
+ const respAwait = jarm ? 'await ' : '';
6
+ const jarmArg = jarm ? 'jarmResponse, ' : '';
7
+ const customScopesDeclared = scopes.length > 0;
8
+ const customScopeImport = customScopesDeclared
9
+ ? `
10
+ import { findUnsupportedScopes, resolveGrantableScopes } from '../_oidc-provider/scopes';`
11
+ : '';
12
+ const customScopeStep = customScopesDeclared
13
+ ? `
14
+ // RFC 6749 §3.3 / §4.1.2.1: this provider declares a scope allow list
15
+ // (scopes.ts), so a value outside it is rejected instead of being carried
16
+ // into the grant. Checked AFTER applyOfflineAccessPolicy on purpose: an
17
+ // offline_access the policy already dropped stays "ignored" (OIDC Core 1.0
18
+ // §11) instead of turning the request into invalid_scope.
19
+ const unsupportedScopes = findUnsupportedScopes(scope);
20
+ if (unsupportedScopes.length > 0) {
21
+ throw new AuthorizationError(
22
+ AuthorizationErrorCode.InvalidScope,
23
+ 'Unsupported scope: ' + unsupportedScopes.join(' '),
24
+ redirectUri,
25
+ state,
26
+ );
27
+ }
28
+ `
29
+ : '';
30
+ const promptNoneScopeStep = customScopesDeclared
31
+ ? `
32
+ // Apply the scope policy BEFORE the consent lookup: looking up consent for
33
+ // a scope this End-User can never hold would answer consent_required
34
+ // forever (OIDC Core 1.0 §3.1.2.1). See resolveGrantableScopes() in
35
+ // scopes.ts.
36
+ transaction.scope = (await resolveGrantableScopes(
37
+ transaction.scope.split(' ').filter(Boolean),
38
+ session.subject,
39
+ )).join(' ');
40
+ `
41
+ : '';
42
+ const ssoScopeStep = customScopesDeclared
43
+ ? ` // Apply the scope policy before the consent lookup, for the same reason
44
+ // as the prompt=none path. The narrowed value is only held in memory:
45
+ // the consent step applies the policy again to what it grants.
46
+ transaction.scope = (await resolveGrantableScopes(
47
+ transaction.scope.split(' ').filter(Boolean),
48
+ existingSession.subject,
49
+ )).join(' ');
50
+
51
+ `
52
+ : '';
53
+ const bindingStoreImport = features.transactionBinding
54
+ ? `
55
+ import { buildTransactionBindingCookie } from '../_oidc-provider/store';`
56
+ : '';
57
+ const transactionCreation = features.transactionBinding
58
+ ? ` // OIDC Core 1.0 §3.1.2.3 / §3.1.2.4: the End-User who authenticates and
59
+ // consents must be the one behind THIS User-Agent. transaction_id alone cannot
60
+ // prove that (it rides in the URL and can leak), so a secret is handed to this
61
+ // browser in an HttpOnly cookie and only its hash is kept on the transaction.
62
+ // See buildTransactionBindingCookie() in store.ts for the threat this closes.
63
+ const bindingSecret = generateRandomString(32);
64
+ const transaction = createAuthTransaction(validatedRequest, csrfToken, {
65
+ bindingHash: await computeTransactionBindingHash(bindingSecret),
66
+ });
67
+ `
68
+ : ` const transaction = createAuthTransaction(validatedRequest, csrfToken);
69
+ `;
70
+ const screenCookieArg = features.transactionBinding
71
+ ? ', buildTransactionBindingCookie(transactionId, bindingSecret, transactionTtlSeconds)'
72
+ : '';
73
+ const redirectToScreenSignature = features.transactionBinding
74
+ ? `function redirectToScreen(
75
+ path: '/login' | '/consent',
76
+ transactionId: string,
77
+ bindingCookie: string,
78
+ ): Response {`
79
+ : `function redirectToScreen(path: '/login' | '/consent', transactionId: string): Response {`;
80
+ const redirectToScreenCookie = features.transactionBinding
81
+ ? `
82
+ response.headers.append('Set-Cookie', bindingCookie);`
83
+ : '';
84
+ const requestObjectStep = features.requestObject
85
+ ? ` // OIDC Core 1.0 §6.1: verify the signed Request Object (request parameter)
86
+ // against the client's registered JWKS and overlay its claims onto the query
87
+ // parameters. RS256 is required; alg=none is accepted only when
88
+ // allowUnsignedRequestObject is enabled (conformance compat).
89
+ // effectiveParams is what every later step validates.
90
+ const { effectiveParams, requestObjectClaims } = await resolveRequestObjectParams(
91
+ params,
92
+ client,
93
+ { allowUnsigned: config.allowUnsignedRequestObject },
94
+ );
95
+ `
96
+ : ` // OIDC Core 1.0 §6.3: the request parameter (Request Object) is disabled in
97
+ // this generated provider; rejectUnsupportedRequestParams below rejects it
98
+ // with request_not_supported. The effective parameters are the query as-is.
99
+ const effectiveParams = params;
100
+ `;
101
+ const rejectUnsupportedStep = features.requestObject
102
+ ? ` // OIDC Core 1.0 §6.3: request_uri / registration are not supported here.
103
+ rejectUnsupportedRequestParams(params, redirectUri, state);
104
+
105
+ // OIDC Core 1.0 §6.1: response_type / client_id inside the Request Object
106
+ // must match the OAuth query parameters.
107
+ validateRequestObjectConsistency(params, requestObjectClaims, redirectUri, state);
108
+ `
109
+ : ` // OIDC Core 1.0 §6.3: request (disabled here) / request_uri / registration
110
+ // are not supported and rejected explicitly.
111
+ rejectUnsupportedRequestParams(params, redirectUri, state, {
112
+ requestParameterSupported: false,
113
+ });
114
+ `;
115
+ const parImports = features.par
116
+ ? `
117
+ import {
118
+ PushedRequestUriError,
119
+ assertPushedRequestUsed,
120
+ resolvePushedRequestUri,
121
+ } from '${EXPERIMENTAL_PACKAGE}/par';
122
+ import { parConfig } from '../par/config';`
123
+ : '';
124
+ const paramsBinding = features.par ? 'let params = rawParams;' : 'const params = rawParams;';
125
+ const parResolveStep = features.par
126
+ ? ` // EXPERIMENTAL — Pushed Authorization Requests (RFC 9126 §4).
127
+ // RFC 9126 §5: with require_pushed_authorization_requests on, a request that
128
+ // did not go through /par is rejected outright.
129
+ if (parConfig.requirePushedAuthorizationRequests) {
130
+ assertPushedRequestUsed(rawParams);
131
+ }
132
+ // Expand a request_uri of the form urn:ietf:params:oauth:request_uri:<ref>
133
+ // into the parameters pushed to /par. The reference is single use and short
134
+ // lived, so reloading this URL fails with invalid_request_uri by design.
135
+ // Anything that is not such a URN (absent, or an OIDC Core §6.2 URL) returns
136
+ // null and is left to the pipeline, which rejects it with
137
+ // request_uri_not_supported.
138
+ const pushedParams = await resolvePushedRequestUri({ params: rawParams, store: parStore });
139
+ if (pushedParams !== null) {
140
+ if (!isAuthorizationRequestParams(pushedParams)) {
141
+ // Defensive: client_id was validated when the request was pushed.
142
+ throw new PushedRequestUriError('invalid_request_uri', 'The request_uri is invalid, expired, or has already been used');
143
+ }
144
+ params = pushedParams;
145
+ }
146
+
147
+ `
148
+ : '';
149
+ const parCatchBranch = features.par
150
+ ? ` if (error instanceof PushedRequestUriError) {
151
+ // RFC 9126 §4 / OIDC Core 1.0 §3.1.2.6: without a resolvable request_uri
152
+ // there is no verified redirect_uri, so this error is NEVER redirected
153
+ // (RFC 6749 §4.1.2.1). Every failure kind (unknown / used / expired /
154
+ // another client's) answers identically, so the response is no existence
155
+ // oracle.
156
+ return nonRedirectableError(request, error.code, error.errorDescription);
157
+ }
158
+ `
159
+ : '';
160
+ const jarmImports = jarm
161
+ ? `
162
+ import {
163
+ buildJarmRedirectUrl,
164
+ createJarmResponseJwt,
165
+ resolveJarmResponseMode,
166
+ type JarmAuthTransactionFields,
167
+ } from '${EXPERIMENTAL_PACKAGE}/jarm';
168
+ import { jarmConfig } from '../_oidc-provider/jarm';`
169
+ : '';
170
+ const jarmResponseBinding = jarm
171
+ ? `
172
+ // EXPERIMENTAL — JARM §2.3. Set once redirect_uri is verified; undefined means
173
+ // the plain query response.
174
+ let jarmResponse: JarmResponseContext | undefined;`
175
+ : '';
176
+ const jarmResolveStep = jarm
177
+ ? ` // EXPERIMENTAL — JARM §2.3: interpret response_mode now that redirect_uri is
178
+ // verified, so an unsupported JWT mode can be reported as a redirectable
179
+ // error. Values outside the \`.jwt\` family stay ignored as before.
180
+ const jarmResolution = resolveJarmResponseMode(effectiveParams);
181
+ if (jarmResolution.kind === 'unsupported-jwt-mode') {
182
+ // JARM §2.3.2 / §2.3.3 (fragment.jwt / form_post.jwt) are not implemented.
183
+ // The rejection itself goes back as a PLAIN query error: the OP cannot
184
+ // answer in a response mode it does not implement.
185
+ throw new AuthorizationError(
186
+ AuthorizationErrorCode.InvalidRequest,
187
+ 'response_mode ' + jarmResolution.requested + ' is not supported',
188
+ redirectUri,
189
+ state,
190
+ );
191
+ }
192
+ if (jarmResolution.kind === 'jarm') {
193
+ // JARM §3: the response JWT declares alg RS256 (the default for a client
194
+ // that registered no authorization_signed_response_alg). The active key is
195
+ // not guaranteed to be RS256, so the key is picked by alg from the
196
+ // registered set — its public half is published at /.well-known/jwks.json
197
+ // under the same kid. selectSigningKeyByAlg throws when no RS256 key is
198
+ // registered, which surfaces as a server_error (a configuration mistake).
199
+ jarmResponse = {
200
+ issuer,
201
+ clientId: client.clientId,
202
+ signingKey: selectSigningKeyByAlg(keys.general.registered, 'RS256'),
203
+ };
204
+ }
205
+
206
+ `
207
+ : '';
208
+ const transactionStorage = jarm
209
+ ? ` // EXPERIMENTAL — JARM: the consent step only sees the transaction it reads
210
+ // back from the store, so the requested response mode is recorded on it (the
211
+ // transaction store MUST persist fields it does not know about).
212
+ const storedTransaction: AuthTransaction & JarmAuthTransactionFields = jarmResponse
213
+ ? { ...transaction, jarmResponseMode: 'query.jwt' }
214
+ : transaction;
215
+ await transactionStore.put('auth_txn:' + transactionId, storedTransaction, transactionTtlSeconds);
216
+ `
217
+ : ` await transactionStore.put('auth_txn:' + transactionId, transaction, transactionTtlSeconds);
218
+ `;
219
+ const responseHelpers = jarm
220
+ ? `/**
221
+ * EXPERIMENTAL — JARM response context (JARM §2.1). Present only for a request
222
+ * that asked for response_mode=query.jwt (or its \`jwt\` shorthand); undefined
223
+ * means the plain query response.
224
+ */
225
+ interface JarmResponseContext {
226
+ issuer: string;
227
+ clientId: string;
228
+ signingKey: SigningKey;
229
+ }
230
+
231
+ /**
232
+ * Send an authorization response back to the client.
233
+ *
234
+ * Plain mode: the parameters ride in the query, with iss appended (RFC 9207 §2).
235
+ * EXPERIMENTAL JARM mode (JARM §2.3.1): the same parameters become claims of one
236
+ * signed JWT in the \`response\` parameter, and no plain parameter is added — the
237
+ * JWT's iss claim identifies the issuer (RFC 9700 §2.1 accepts JARM for that).
238
+ */
239
+ async function redirectToClient(
240
+ jarm: JarmResponseContext | undefined,
241
+ redirectUri: string,
242
+ parameters: Record<string, string | undefined>,
243
+ ): Promise<Response> {
244
+ if (jarm) {
245
+ const location = buildJarmRedirectUrl(
246
+ redirectUri,
247
+ await createJarmResponseJwt({
248
+ issuer: jarm.issuer,
249
+ clientId: jarm.clientId,
250
+ parameters,
251
+ signingKey: jarm.signingKey,
252
+ lifetimeSeconds: jarmConfig.jarmResponseLifetimeSeconds,
253
+ }),
254
+ );
255
+ return NextResponse.redirect(location, 302);
256
+ }
257
+ const url = new URL(redirectUri);
258
+ for (const [name, value] of Object.entries(parameters)) {
259
+ if (value !== undefined) url.searchParams.set(name, value);
260
+ }
261
+ url.searchParams.set('iss', config.issuer);
262
+ return NextResponse.redirect(url, 302);
263
+ }
264
+
265
+ /**
266
+ * A redirectable error (OIDC Core 1.0 §3.1.2.6 / RFC 6749 §4.1.2.1).
267
+ * error_description is reduced to the RFC 6749 §5.2 character set, so a
268
+ * user-controlled fragment cannot smuggle control bytes into the URL.
269
+ */
270
+ function errorRedirect(
271
+ jarm: JarmResponseContext | undefined,
272
+ redirectUri: string,
273
+ error: string,
274
+ state?: string,
275
+ errorDescription?: string,
276
+ ): Promise<Response> {
277
+ return redirectToClient(jarm, redirectUri, {
278
+ error,
279
+ error_description: errorDescription ? sanitizeErrorDescription(errorDescription) : undefined,
280
+ state,
281
+ });
282
+ }
283
+
284
+ /** The authorization response carrying the code (OIDC Core 1.0 §3.1.2.5). */
285
+ function successRedirect(
286
+ jarm: JarmResponseContext | undefined,
287
+ redirectUri: string,
288
+ code: string,
289
+ state: string | undefined,
290
+ ): Promise<Response> {
291
+ return redirectToClient(jarm, redirectUri, { code, state });
292
+ }`
293
+ : `/**
294
+ * A redirectable error (OIDC Core 1.0 §3.1.2.6 / RFC 6749 §4.1.2.1), with iss
295
+ * (RFC 9207 §2). error_description is reduced to the RFC 6749 §5.2 character
296
+ * set, so a user-controlled fragment cannot smuggle control bytes into the URL.
297
+ */
298
+ function errorRedirect(
299
+ redirectUri: string,
300
+ error: string,
301
+ state?: string,
302
+ errorDescription?: string,
303
+ ): Response {
304
+ const url = new URL(redirectUri);
305
+ url.searchParams.set('error', error);
306
+ if (errorDescription) {
307
+ url.searchParams.set('error_description', sanitizeErrorDescription(errorDescription));
308
+ }
309
+ if (state) url.searchParams.set('state', state);
310
+ url.searchParams.set('iss', config.issuer);
311
+ return NextResponse.redirect(url, 302);
312
+ }
313
+
314
+ /** The authorization response carrying the code (OIDC Core 1.0 §3.1.2.5), with iss (RFC 9207 §2). */
315
+ function successRedirect(redirectUri: string, code: string, state: string | undefined): Response {
316
+ const url = new URL(redirectUri);
317
+ url.searchParams.set('code', code);
318
+ if (state) url.searchParams.set('state', state);
319
+ url.searchParams.set('iss', config.issuer);
320
+ return NextResponse.redirect(url, 302);
321
+ }`;
322
+ const offlineAccessStep = features.refreshToken
323
+ ? ` // offline_access は 2 つの独立した条件を両方満たしたときだけ残る。
324
+ // - OIDC Core 1.0 §11: エンドユーザーの同意(prompt=consent)
325
+ // - RFC 7591 §2: クライアント登録の grant_types に refresh_token があること
326
+ // (既定は ["authorization_code"])。無いまま offline_access を通すと、発行した
327
+ // Refresh Token が unauthorized_client で拒否されるだけの死んだ資格情報になる。
328
+ // 独自の許可条件を差し込むならコールバックを渡す(client も受け取れる):
329
+ // scope = await applyOfflineAccessPolicy(scope, effectiveParams, prompt, client,
330
+ // (req, { promptValues }) => promptValues.includes('consent') || hasStoredConsent(req));
331
+ scope = await applyOfflineAccessPolicy(scope, effectiveParams, prompt, client);
332
+ `
333
+ : ` // The refresh_token feature is disabled in this generated provider: the
334
+ // callback always returns false, so offline_access is never granted
335
+ // (OIDC Core 1.0 §11 requires ignoring the request in that case).
336
+ scope = await applyOfflineAccessPolicy(scope, effectiveParams, prompt, client, () => false);
337
+ `;
338
+ const coreImports = [
339
+ 'resolveClientForAuthorization',
340
+ 'validateRegisteredRedirectUris',
341
+ ...(features.requestObject
342
+ ? ['resolveRequestObjectParams', 'validateRequestObjectConsistency']
343
+ : []),
344
+ 'resolveAuthorizationRedirectUri',
345
+ 'rejectUnsupportedRequestParams',
346
+ 'validateResponseType',
347
+ 'validateAuthorizationScope',
348
+ 'validateAuthorizationCodePkce',
349
+ 'validatePromptParameter',
350
+ 'applyOfflineAccessPolicy',
351
+ 'validateDisplayParameter',
352
+ 'resolveMaxAge',
353
+ 'parseAudienceParameter',
354
+ 'parseClaimsRequestParameter',
355
+ 'validateIdTokenHint',
356
+ 'createAuthTransaction',
357
+ ...(features.transactionBinding ? ['computeTransactionBindingHash'] : []),
358
+ 'createAuthorizationCode',
359
+ 'completeAuthTransaction',
360
+ 'generateRandomString',
361
+ 'resolvePromptNoneSession',
362
+ 'validatePromptNoneIdTokenHint',
363
+ 'validatePromptNoneConsent',
364
+ 'requiresReauthentication',
365
+ 'sanitizeErrorDescription',
366
+ ...(jarm ? ['selectSigningKeyByAlg'] : []),
367
+ 'AuthorizationError',
368
+ ...(jarm || customScopesDeclared ? ['AuthorizationErrorCode'] : []),
369
+ 'IdTokenHintError',
370
+ ...(jarm ? ['type AuthTransaction'] : []),
371
+ 'type AuthorizationRequestParams',
372
+ ...(jarm ? ['type SigningKey'] : []),
373
+ ];
374
+ const parStoreImport = features.par ? '\n parStore,' : '';
375
+ return `/**
376
+ * Authorization Endpoint (OIDC Core 1.0 §3.1.2).
377
+ *
378
+ * The browser's entry into the flow, so every answer is a redirect: back to the
379
+ * client with the authorization response, on to the login or consent screen,
380
+ * or — for an error that must not go back to the client — to the OP's own
381
+ * error page (app/oidc-error).
382
+ */
383
+ import { NextResponse, type NextRequest } from 'next/server';
384
+ import {
385
+ ${coreImports.map((name) => ` ${name},`).join('\n')}
386
+ } from '${corePkg}';${parImports}${jarmImports}
387
+ import {
388
+ clientResolver,
389
+ config,
390
+ idTokenHintJwks,
391
+ loadSigningKeys,${parStoreImport}
392
+ resolvers,
393
+ stores,
394
+ } from '../_oidc-provider/provider';
395
+ import {
396
+ isFormUrlEncoded,
397
+ redirectToErrorPage,
398
+ signingKeysUnavailable,
399
+ uniqueParams,
400
+ type UniqueParams,
401
+ } from '../_oidc-provider/http';${bindingStoreImport}${customScopeImport}
402
+
403
+ export const dynamic = 'force-dynamic';
404
+ export const runtime = 'nodejs';
405
+
406
+ const { transactionStore, authCodeStore, authSessionStore } = stores;
407
+ const { sessionResolver, consentResolver } = resolvers;
408
+
409
+ /** OIDC Core 1.0 §3.1.2.1: the Authorization Endpoint MUST support GET ... */
410
+ export async function GET(request: NextRequest): Promise<Response> {
411
+ return authorize(request, uniqueParams(request.nextUrl.searchParams));
412
+ }
413
+
414
+ /** ... and POST, with a form-encoded body (OIDC Core 1.0 §13.2). */
415
+ export async function POST(request: NextRequest): Promise<Response> {
416
+ if (!isFormUrlEncoded(request)) {
417
+ return badRequest('Authorization POST requests must use application/x-www-form-urlencoded');
418
+ }
419
+ return authorize(request, uniqueParams(new URLSearchParams(await request.text())));
420
+ }
421
+
422
+ async function authorize(request: NextRequest, parsed: UniqueParams): Promise<Response> {
423
+ const keys = await loadSigningKeys().catch(() => null);
424
+ if (!keys) return signingKeysUnavailable();
425
+
426
+ // OIDC Core 1.0 §3.1.2.1 / RFC 6749 §3.1: request parameters MUST NOT be repeated.
427
+ if (parsed.duplicateKey !== undefined) {
428
+ return badRequest(\`Parameter "\${parsed.duplicateKey}" must not be repeated\`);
429
+ }
430
+
431
+ const rawParams = parsed.params;
432
+ if (!isAuthorizationRequestParams(rawParams)) {
433
+ return badRequest('Missing required parameter: client_id');
434
+ }
435
+
436
+ ${paramsBinding}${jarmResponseBinding}
437
+ // RFC 9207 §2: every authorization response (success and error) carries the
438
+ // issuer identifier, so clients can pin the issuer that produced it.
439
+ const issuer = config.issuer;
440
+
441
+ try {
442
+ ${parResolveStep} // --- Authorization request validation pipeline ---------------------------
443
+ // Each step below is an independent core function, called in the same order
444
+ // as core's validateAuthorizationRequest(). Delete a call to drop that
445
+ // validation, or insert your own logic between steps. Steps that run before
446
+ // redirectUri is resolved throw non-redirectable errors (shown on the OP);
447
+ // steps after it throw redirectable errors (sent back to the client).
448
+
449
+ // OAuth 2.1 §4.1.2.1: resolve client_id into the registered client.
450
+ const client = await resolveClientForAuthorization(params, clientResolver);
451
+
452
+ // Fail fast on misconfigured registered redirect URIs (fragments, dangerous
453
+ // schemes, non-loopback http) — OIDC Core 1.0 §3.1.2.1 / RFC 8252 §8.
454
+ validateRegisteredRedirectUris(client.redirectUris);
455
+
456
+ ${requestObjectStep}
457
+ // Resolve redirect_uri against the registered URIs (OIDC Core 1.0 §3.1.2.1).
458
+ const redirectUri = resolveAuthorizationRedirectUri(effectiveParams, client);
459
+ // RFC 6749 §4.1.2.1: state is echoed only on redirectable errors from here on.
460
+ const state = effectiveParams.state;
461
+
462
+ ${jarmResolveStep}${rejectUnsupportedStep}
463
+ // response_type=code and per-client response_type authorization.
464
+ const responseType = validateResponseType(params, client, redirectUri, state);
465
+
466
+ // scope must be in the query (OIDC Core 1.0 §6.1) and contain openid (§3.1.2.1).
467
+ let scope = validateAuthorizationScope(params, effectiveParams, redirectUri, state);
468
+
469
+ // OAuth 2.1 §4.1.1 / §7.5: PKCE with S256 (allowNonPkceAuthorizationCodeFlow
470
+ // exists only for the OIDF Basic OP static-client compatibility target).
471
+ const pkce = validateAuthorizationCodePkce(effectiveParams, client, redirectUri, state, {
472
+ allowNonPkceAuthorizationCodeFlow: config.allowNonPkceAuthorizationCodeFlow,
473
+ });
474
+
475
+ // OIDC Core 1.0 §3.1.2.1: prompt is none|login|consent|select_account.
476
+ const prompt = validatePromptParameter(effectiveParams, redirectUri, state);
477
+
478
+ ${offlineAccessStep}${customScopeStep}
479
+ // OIDC Core 1.0 §3.1.2.1: display is page|popup|touch|wap.
480
+ const display = validateDisplayParameter(effectiveParams, redirectUri, state);
481
+
482
+ // OIDC Core 1.0 §3.1.2.1 / Dynamic Client Registration 1.0 §2: max_age from
483
+ // the request, falling back to the client's registered default_max_age.
484
+ const maxAge = resolveMaxAge(effectiveParams, client, redirectUri, state);
485
+
486
+ // Space-delimited audience for the access token.
487
+ const audience = parseAudienceParameter(effectiveParams);
488
+
489
+ // OIDC Core 1.0 §5.5: parse the claims request parameter (userinfo / id_token).
490
+ const claims = parseClaimsRequestParameter(effectiveParams, redirectUri, state);
491
+
492
+ // Assemble the validated request from each step's result. This shape matches
493
+ // core's validateAuthorizationRequest(), so transactions and authorization
494
+ // codes are unaffected by adding or removing steps above.
495
+ const validatedRequest = {
496
+ responseType,
497
+ clientId: client.clientId,
498
+ redirectUri,
499
+ // OIDC Core 1.0 §3.1.3.2: when redirect_uri was sent explicitly, the token
500
+ // request must repeat it; remember which case produced this authorization.
501
+ redirectUriExplicit: effectiveParams.redirect_uri !== undefined,
502
+ scope,
503
+ codeChallenge: pkce.codeChallenge,
504
+ codeChallengeMethod: pkce.codeChallengeMethod,
505
+ state,
506
+ nonce: effectiveParams.nonce,
507
+ prompt,
508
+ display,
509
+ maxAge,
510
+ uiLocales: effectiveParams.ui_locales,
511
+ claimsLocales: effectiveParams.claims_locales,
512
+ acrValues: effectiveParams.acr_values,
513
+ loginHint: effectiveParams.login_hint,
514
+ idTokenHint: effectiveParams.id_token_hint,
515
+ audience,
516
+ claims,
517
+ };
518
+
519
+ // The authentication transaction carries the request through login and consent.
520
+ const csrfToken = generateRandomString(32);
521
+ ${transactionCreation} const transactionId = generateRandomString(32);
522
+ const transactionTtlSeconds = 10 * 60;
523
+ ${transactionStorage}
524
+ // OIDC Core 1.0 §3.1.2.1: prompt is a space-delimited list.
525
+ const promptValues = transaction.prompt?.trim().split(/\\s+/).filter(Boolean) ?? [];
526
+
527
+ // prompt=none must not be combined with other values (OIDC Core 1.0 §3.1.2.1).
528
+ if (promptValues.includes('none') && promptValues.length > 1) {
529
+ await transactionStore.delete('auth_txn:' + transactionId);
530
+ return ${respAwait}errorRedirect(${jarmArg}transaction.redirectUri, 'invalid_request', transaction.state, 'prompt=none must not be combined with other prompt values');
531
+ }
532
+
533
+ // OIDC Core 1.0 §3.1.2.1: the id_token_hint rule ("if the End-User identified
534
+ // by the ID Token is logged in ... otherwise it SHOULD return an error") is NOT
535
+ // conditioned on prompt, so the hint is verified here — outside the prompt=none
536
+ // branch — and therefore on every prompt path. Verification covers signature,
537
+ // iss, aud, exp and iat; the verified subject is shared by the prompt=none
538
+ // check and the SSO fast path below, so an unverified hint never reaches a
539
+ // session decision.
540
+ let verifiedHintSubject: string | undefined;
541
+ if (transaction.idTokenHint !== undefined) {
542
+ try {
543
+ const verified = await validateIdTokenHint(transaction.idTokenHint, {
544
+ expectedIss: issuer,
545
+ expectedAud: transaction.clientId,
546
+ jwks: await idTokenHintJwks(keys),
547
+ });
548
+ verifiedHintSubject = verified.sub;
549
+ } catch (hintError) {
550
+ await transactionStore.delete('auth_txn:' + transactionId);
551
+ const code = hintError instanceof IdTokenHintError ? hintError.error : 'login_required';
552
+ return ${respAwait}errorRedirect(${jarmArg}transaction.redirectUri, code, transaction.state, hintError instanceof Error && hintError.message ? hintError.message : 'id_token_hint verification failed');
553
+ }
554
+ }
555
+
556
+ // prompt=none: silent authentication without any user interaction
557
+ // (OIDC Core 1.0 §3.1.2.1).
558
+ if (promptValues.includes('none')) {
559
+ let session;
560
+ try {
561
+ // --- prompt=none pipeline -------------------------------------------
562
+ // Each step below is an independent core function, called in the same
563
+ // order as core's checkPromptNone(). Every step throws
564
+ // AuthorizationError(login_required | consent_required) on failure.
565
+
566
+ // OIDC Core 1.0 §3.1.2.1: no active session → login_required (the OP
567
+ // must not show a login screen for prompt=none).
568
+ session = await resolvePromptNoneSession(transaction, sessionResolver, request);
569
+
570
+ // verifiedHintSubject は上流(prompt 非依存の検証ブロック)で確定済み。
571
+ // ここでは prompt=none 固有の「不一致なら login_required」判定だけを行う。
572
+ // コンセント確認より前に置くのは、コンセント検索が session.subject をキーに
573
+ // するため — 不一致のまま進むと別ユーザーのコンセントを見てしまう。
574
+ validatePromptNoneIdTokenHint(transaction, session, verifiedHintSubject);
575
+ ${promptNoneScopeStep}
576
+ // OIDC Core 1.0 §3.1.2.1: not consented → consent_required (the OP must
577
+ // not show a consent screen for prompt=none).
578
+ await validatePromptNoneConsent(transaction, session, consentResolver);
579
+ } catch (promptError) {
580
+ await transactionStore.delete('auth_txn:' + transactionId);
581
+ if (promptError instanceof AuthorizationError) {
582
+ return ${respAwait}errorRedirect(${jarmArg}transaction.redirectUri, promptError.error, transaction.state, promptError.errorDescription);
583
+ }
584
+ const serverDescription =
585
+ promptError instanceof Error && promptError.message
586
+ ? promptError.message
587
+ : 'Unexpected error while evaluating prompt=none';
588
+ return ${respAwait}errorRedirect(${jarmArg}transaction.redirectUri, 'server_error', transaction.state, serverDescription);
589
+ }
590
+
591
+ // OIDC Core 1.0 §3.1.2.1: a session older than max_age cannot be refreshed
592
+ // without interaction, which prompt=none forbids.
593
+ if (transaction.maxAge !== undefined && requiresReauthentication(transaction.maxAge, session.authTime)) {
594
+ await transactionStore.delete('auth_txn:' + transactionId);
595
+ return ${respAwait}errorRedirect(${jarmArg}transaction.redirectUri, 'login_required', transaction.state, 'Session exceeds the requested max_age; re-authentication required');
596
+ }
597
+
598
+ // transaction.scope は認可リクエスト検証時に applyOfflineAccessPolicy を通した
599
+ // 後の値。offline_access の可否(OIDC Core 1.0 §11 の prompt=consent と、
600
+ // クライアント登録 grant_types に refresh_token があるか)はそこで判定済みなので、
601
+ // ここで再フィルタしない。
602
+ const grantedScope = transaction.scope.split(' ').filter(Boolean);
603
+
604
+ const responseParams = await completeAuthTransaction(
605
+ transactionId,
606
+ transaction,
607
+ transactionStore,
608
+ );
609
+ const authCodeData = await createAuthorizationCode({
610
+ authorizationResponse: { ...responseParams, scope: grantedScope },
611
+ subject: session.subject,
612
+ authTime: session.authTime,
613
+ // online refresh token をこのログインセッションへ束縛するために引き継ぐ。
614
+ // セッションが終われば、その RT は invalid_grant になる。
615
+ sessionId: session.sessionId,
616
+ // OIDC Core 1.0 §3.1.3.1: TTL は ProviderConfig から設定可能(既定 300 秒)。
617
+ ttlSeconds: config.authorizationCodeTtl,
618
+ });
619
+ await authCodeStore.set(authCodeData.code, authCodeData);
620
+ await consentResolver.recordGrant(session.subject, transaction.clientId, authCodeData.grantId);
621
+
622
+ return ${respAwait}successRedirect(${jarmArg}transaction.redirectUri, authCodeData.code, transaction.state);
623
+ }
624
+
625
+ // OIDC Core 1.0 §3.1.2.3: an active OP session enables Single Sign-On, so it
626
+ // is reused (skipping the login screen) unless prompt forces fresh
627
+ // authentication. With max_age the session must also be fresh enough
628
+ // (§3.1.2.1). prompt=login / prompt=select_account always re-authenticate.
629
+ if (!promptValues.includes('login') && !promptValues.includes('select_account')) {
630
+ const existingSession = await sessionResolver.resolve(request);
631
+ const sessionIsFresh =
632
+ existingSession !== null &&
633
+ (transaction.maxAge === undefined ||
634
+ !requiresReauthentication(transaction.maxAge, existingSession.authTime));
635
+ // OIDC Core 1.0 §3.1.2.1: id_token_hint が指す End-User でなければ既存
636
+ // セッションを再利用しない。これが無いと「セッションは User B / hint は
637
+ // User A」の要求に対し B の認可コードを黙って発行してしまう。
638
+ // 不一致はエラーにせずログイン画面へ落とし、正しい End-User として認証さ
639
+ // せる(login_required を即返すかは方針判断に委ねる)。
640
+ const hintMatchesSession =
641
+ verifiedHintSubject === undefined ||
642
+ (existingSession !== null && verifiedHintSubject === existingSession.subject);
643
+ if (existingSession && sessionIsFresh && hintMatchesSession) {
644
+ ${ssoScopeStep} // OIDC Core 1.0 §3.1.2.1: prompt=consent MUST re-display the consent UI.
645
+ // Otherwise, when the End-User already granted (a superset of) the
646
+ // requested scopes to this client, the code is issued right away — the
647
+ // interactive analogue of the prompt=none path.
648
+ const consentAlreadyGranted =
649
+ !promptValues.includes('consent') &&
650
+ (await consentResolver.hasConsent(
651
+ existingSession.subject,
652
+ transaction.clientId,
653
+ transaction.scope.split(' ').filter(Boolean),
654
+ ));
655
+
656
+ if (consentAlreadyGranted) {
657
+ // transaction.scope は applyOfflineAccessPolicy 通過後の値(prompt=consent と
658
+ // クライアントの grant_types で offline_access の可否は判定済み)。再フィルタしない。
659
+ const grantedScope = transaction.scope.split(' ').filter(Boolean);
660
+
661
+ const responseParams = await completeAuthTransaction(
662
+ transactionId,
663
+ transaction,
664
+ transactionStore,
665
+ );
666
+ const authCodeData = await createAuthorizationCode({
667
+ authorizationResponse: { ...responseParams, scope: grantedScope },
668
+ subject: existingSession.subject,
669
+ authTime: existingSession.authTime,
670
+ // online refresh token を、この SSO で再利用したログインセッションへ束縛する。
671
+ sessionId: existingSession.sessionId,
672
+ // OIDC Core 1.0 §3.1.3.1: TTL は ProviderConfig から設定可能(既定 300 秒)。
673
+ ttlSeconds: config.authorizationCodeTtl,
674
+ });
675
+ await authCodeStore.set(authCodeData.code, authCodeData);
676
+ await consentResolver.recordGrant(
677
+ existingSession.subject,
678
+ transaction.clientId,
679
+ authCodeData.grantId,
680
+ );
681
+
682
+ return ${respAwait}successRedirect(${jarmArg}transaction.redirectUri, authCodeData.code, transaction.state);
683
+ }
684
+
685
+ // Signed in already: hand the subject to the consent step.
686
+ await authSessionStore.set(transactionId, {
687
+ subject: existingSession.subject,
688
+ authTime: existingSession.authTime,
689
+ // consent 画面を経由しても online refresh token の束縛先を見失わないよう、
690
+ // login → consent の受け渡しに sessionId も載せる。
691
+ sessionId: existingSession.sessionId,
692
+ });
693
+ return redirectToScreen('/consent', transactionId${screenCookieArg});
694
+ }
695
+ }
696
+
697
+ // Interactive authentication (prompt=login forces it even with a session).
698
+ return redirectToScreen('/login', transactionId${screenCookieArg});
699
+ } catch (error) {
700
+ ${parCatchBranch} if (error instanceof AuthorizationError) {
701
+ if (error.redirectUri) {
702
+ return ${respAwait}errorRedirect(${jarmArg}error.redirectUri, error.error, error.state, error.errorDescription);
703
+ }
704
+ // OIDC Core 1.0 §3.1.2.2: an error that cannot be redirected (unknown
705
+ // client_id, unregistered redirect_uri, redirect_uri with a fragment) MUST
706
+ // NOT go to the supplied redirect_uri.
707
+ return nonRedirectableError(request, error.error, error.errorDescription);
708
+ }
709
+ return Response.json({ error: 'server_error' }, { status: 500 });
710
+ }
711
+ }
712
+
713
+ /**
714
+ * Narrows raw parameters to AuthorizationRequestParams. PKCE parameters are
715
+ * validated by core, so conformance compatibility mode can pass requests that
716
+ * omit them.
717
+ */
718
+ function isAuthorizationRequestParams(params: unknown): params is AuthorizationRequestParams {
719
+ if (typeof params !== 'object' || params === null) return false;
720
+ return typeof (params as Record<string, unknown>)['client_id'] === 'string';
721
+ }
722
+
723
+ /** Malformed transport: OAuth error JSON, there is no transaction to show a screen for. */
724
+ function badRequest(errorDescription: string): Response {
725
+ return Response.json({ error: 'invalid_request', error_description: errorDescription }, { status: 400 });
726
+ }
727
+
728
+ /**
729
+ * Continue on one of the OP's own screens. The URL is built on config.issuer,
730
+ * never on the request URL: some platforms derive the request URL from the Host
731
+ * header, which would let the sender choose where transaction_id lands
732
+ * (OIDC Discovery 1.0 §3 / RFC 9700 §2.1).
733
+ */
734
+ ${redirectToScreenSignature}
735
+ const url = new URL(path, config.issuer);
736
+ url.searchParams.set('transaction_id', transactionId);
737
+ const response = NextResponse.redirect(url, 302);${redirectToScreenCookie}
738
+ return response;
739
+ }
740
+
741
+ /**
742
+ * OIDC Core 1.0 §3.1.2.2: an error that cannot be sent back to the client stays
743
+ * on the OP. Programmatic callers asking for JSON get the OAuth error JSON;
744
+ * browsers are sent to the OP's error page (app/oidc-error/page.tsx), which the
745
+ * OIDF Conformance Suite screenshots for oidcc-ensure-registered-redirect-uri.
746
+ */
747
+ function nonRedirectableError(request: Request, error: string, errorDescription?: string): Response {
748
+ if ((request.headers.get('Accept') ?? '').includes('application/json')) {
749
+ return Response.json({ error, error_description: errorDescription }, { status: 400 });
750
+ }
751
+ return redirectToErrorPage(error, errorDescription);
752
+ }
753
+
754
+ ${responseHelpers}
755
+ `;
756
+ }
757
+ //# sourceMappingURL=authorize.js.map