@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,1337 @@
1
+ import { DEFAULT_FEATURES } from '../../features.js';
2
+ import { EXPERIMENTAL_PACKAGE } from '../hono/templates.js';
3
+ const EXPERIMENTAL_NOTICE = ` * Backed by ${EXPERIMENTAL_PACKAGE}, whose API is NOT stable: it may change in
4
+ * a breaking way between releases. Do not build production code on it without
5
+ * pinning the version.`;
6
+ export function nextJsTokenRouteTemplate(corePkg, features = DEFAULT_FEATURES) {
7
+ const refresh = features.refreshToken;
8
+ const experimentalGrants = [
9
+ ...(features.tokenExchange ? ['token exchange (token-exchange.ts)'] : []),
10
+ ...(features.idJag ? ['ID-JAG issuance and redemption (id-jag.ts)'] : []),
11
+ ...(features.deviceAuthorizationGrant ? ['device_code (device-code.ts)'] : []),
12
+ ...(features.ciba ? ['CIBA (ciba.ts)'] : []),
13
+ ];
14
+ const experimentalGrantDoc = experimentalGrants.length > 0
15
+ ? `
16
+ *
17
+ * The experimental grants live in the modules next to this file, each
18
+ * dispatched right after client authentication and before core's
19
+ * validateGrantTypeSupported (which does not know their URNs):
20
+ ${experimentalGrants.map((grant) => ` * - ${grant}`).join('\n')}`
21
+ : '';
22
+ const coreImports = [
23
+ 'validateGrantTypeSupported',
24
+ 'resolveAuthenticatedTokenClient',
25
+ 'validateClientGrantType',
26
+ 'resolveAuthorizationCode',
27
+ 'validateAuthorizationCodeUnused',
28
+ 'validateAuthorizationCodeClient',
29
+ 'validateAuthorizationCodeExpiration',
30
+ 'validateAuthorizationCodeRedirectUri',
31
+ 'verifyAuthorizationCodePkce',
32
+ 'consumeAuthorizationCode',
33
+ 'buildValidatedAuthorizationCodeRequest',
34
+ ...(refresh
35
+ ? [
36
+ 'resolveRefreshToken',
37
+ 'validateRefreshTokenUnused',
38
+ 'validateRefreshTokenClient',
39
+ 'validateRefreshTokenExpiration',
40
+ 'validateRefreshTokenIdleTimeout',
41
+ 'validateRefreshTokenScope',
42
+ 'validateRefreshTokenSession',
43
+ 'clientAllowsRefreshTokenGrant',
44
+ 'buildValidatedRefreshTokenRequest',
45
+ ]
46
+ : []),
47
+ 'buildAccessTokenPayload',
48
+ 'computeAtHash',
49
+ 'resolveAcrAmr',
50
+ 'buildIdTokenPayload',
51
+ 'generateIdToken',
52
+ ...(refresh ? ['generateRandomString'] : []),
53
+ 'buildAccessTokenAudience',
54
+ 'extractClientCredentials',
55
+ 'validateClientAuthMethod',
56
+ 'verifyClientSecret',
57
+ 'TokenError',
58
+ 'TokenErrorCode',
59
+ 'type AuthorizationCodeData',
60
+ 'type TokenRequestParams',
61
+ 'type ValidatedTokenRequest',
62
+ ];
63
+ const experimentalImports = [
64
+ ...(features.tokenExchange
65
+ ? [`import { TOKEN_EXCHANGE_GRANT_TYPE, TokenExchangeError } from '${EXPERIMENTAL_PACKAGE}/token-exchange';`]
66
+ : []),
67
+ ...(features.idJag
68
+ ? [`import {
69
+ IdJagError,
70
+ JWT_BEARER_GRANT_TYPE,${features.tokenExchange ? '' : '\n ID_JAG_TOKEN_TYPE,\n TOKEN_EXCHANGE_GRANT_TYPE,'}
71
+ matchesIdJagIssuanceRequest,
72
+ } from '${EXPERIMENTAL_PACKAGE}/id-jag';`]
73
+ : []),
74
+ ...(features.deviceAuthorizationGrant
75
+ ? [`import {
76
+ DEVICE_CODE_GRANT_TYPE,
77
+ DeviceAuthorizationError,
78
+ } from '${EXPERIMENTAL_PACKAGE}/device-authorization-grant';`]
79
+ : []),
80
+ ...(features.ciba
81
+ ? [`import { CIBA_GRANT_TYPE, CibaGrantError } from '${EXPERIMENTAL_PACKAGE}/ciba';`]
82
+ : []),
83
+ ];
84
+ const grantModuleImports = [
85
+ ...(features.tokenExchange ? [`import { exchangeToken } from './token-exchange';`] : []),
86
+ ...(features.idJag ? [`import { issueIdJag, redeemIdJag } from './id-jag';`] : []),
87
+ ...(features.deviceAuthorizationGrant ? [`import { redeemDeviceCode } from './device-code';`] : []),
88
+ ...(features.ciba ? [`import { redeemCibaRequest } from './ciba';`] : []),
89
+ ];
90
+ const importBlock = [...experimentalImports, ...grantModuleImports];
91
+ const experimentalImportBlock = importBlock.length > 0 ? `\n${importBlock.join('\n')}` : '';
92
+ const experimentalDispatch = [
93
+ ...(features.idJag
94
+ ? [` // EXPERIMENTAL — ID-JAG issuance (Cross-App Access, draft §4.3): a token
95
+ // exchange whose requested_token_type is the ID-JAG URN. Checked BEFORE the
96
+ // plain token exchange, which shares the grant_type URN.
97
+ if (matchesIdJagIssuanceRequest(params)) {
98
+ return await issueIdJag(params, tokenClient, keys);
99
+ }
100
+ ${features.tokenExchange ? '' : `
101
+ // Generated without --enable token-exchange: the exchange grant exists only to
102
+ // issue ID-JAGs, so any other requested_token_type is answered with a pointer
103
+ // instead of falling through to unsupported_grant_type (discovery does
104
+ // advertise the exchange grant in this build).
105
+ if (params.grant_type === TOKEN_EXCHANGE_GRANT_TYPE) {
106
+ return oauthError(
107
+ 'invalid_request',
108
+ \`This authorization server only supports requested_token_type \${ID_JAG_TOKEN_TYPE} for token exchange\`,
109
+ );
110
+ }
111
+ `}
112
+ // EXPERIMENTAL — ID-JAG redemption on the jwt-bearer grant (draft §4.4).
113
+ if (params.grant_type === JWT_BEARER_GRANT_TYPE) {
114
+ return await redeemIdJag(params, tokenClient, keys);
115
+ }
116
+ `]
117
+ : []),
118
+ ...(features.tokenExchange
119
+ ? [` // EXPERIMENTAL — OAuth 2.0 Token Exchange (RFC 8693 §2.1).
120
+ if (params.grant_type === TOKEN_EXCHANGE_GRANT_TYPE) {
121
+ return await exchangeToken(params, tokenClient, keys);
122
+ }
123
+ `]
124
+ : []),
125
+ ...(features.deviceAuthorizationGrant
126
+ ? [` // EXPERIMENTAL — OAuth 2.0 Device Authorization Grant (RFC 8628 §3.4).
127
+ if (params.grant_type === DEVICE_CODE_GRANT_TYPE) {
128
+ return await redeemDeviceCode(params, tokenClient, keys);
129
+ }
130
+ `]
131
+ : []),
132
+ ...(features.ciba
133
+ ? [` // EXPERIMENTAL — CIBA grant (CIBA Core 1.0 §10.1, poll mode).
134
+ if (params.grant_type === CIBA_GRANT_TYPE) {
135
+ return await redeemCibaRequest(params, tokenClient, keys);
136
+ }
137
+ `]
138
+ : []),
139
+ ];
140
+ const experimentalDispatchBlock = experimentalDispatch.length > 0
141
+ ? `\n${experimentalDispatch.join('\n')}`
142
+ : '';
143
+ const experimentalErrorClasses = [
144
+ ...(features.idJag ? ['IdJagError'] : []),
145
+ ...(features.tokenExchange ? ['TokenExchangeError'] : []),
146
+ ...(features.deviceAuthorizationGrant ? ['DeviceAuthorizationError'] : []),
147
+ ...(features.ciba ? ['CibaGrantError'] : []),
148
+ ];
149
+ const experimentalErrorMapping = experimentalErrorClasses.length > 0
150
+ ? ` if (
151
+ ${experimentalErrorClasses.map((name) => ` error instanceof ${name}`).join(' ||\n')}
152
+ ) {
153
+ // The experimental grants answer in the RFC 6749 §5.2 shape and always with
154
+ // 400: a 401 can only come from client authentication, which runs before
155
+ // them and throws core's TokenError.
156
+ return oauthError(error.code, error.errorDescription, error.statusCode);
157
+ }
158
+ `
159
+ : '';
160
+ const resolverNames = [
161
+ 'authorizationCodeResolver',
162
+ ...(refresh ? ['refreshTokenResolver', 'authenticationSessionResolver'] : []),
163
+ ];
164
+ const storeNames = ['authCodeStore', 'accessTokenStore', ...(refresh ? ['refreshTokenStore'] : [])];
165
+ const grantTypeSupportedStep = refresh
166
+ ? ` // RFC 6749 §5.2: is the grant_type offered by this OP at all?
167
+ // (defaults to ['authorization_code', 'refresh_token'])
168
+ const grantType = validateGrantTypeSupported(params.grant_type);
169
+ `
170
+ : ` // The refresh_token feature is disabled: the OP only offers the
171
+ // authorization_code grant, so refresh_token requests are rejected with
172
+ // unsupported_grant_type (RFC 6749 §5.2).
173
+ const grantType = validateGrantTypeSupported(params.grant_type, ['authorization_code']);
174
+ `;
175
+ const grantValidationStep = refresh
176
+ ? ` // Grant-specific validation. Each security rule is a separate core call so
177
+ // it can be removed, replaced, or surrounded with experiment-specific logic.
178
+ let validatedRequest: ValidatedTokenRequest;
179
+ if (grantType === 'refresh_token') {
180
+ // Resolve the presented refresh token and retain its stored grant context.
181
+ const { refreshTokenInfo } = await resolveRefreshToken(params, refreshTokenResolver);
182
+
183
+ // OAuth 2.1 §4.3.1: reject rotation reuse and revoke the token family.
184
+ await validateRefreshTokenUnused(refreshTokenInfo, refreshTokenResolver);
185
+
186
+ // Bind the refresh token to the authenticated client.
187
+ validateRefreshTokenClient(refreshTokenInfo, authenticatedClientId);
188
+
189
+ // Absolute lifetime: expiresAt <= now is expired.
190
+ validateRefreshTokenExpiration(refreshTokenInfo);
191
+
192
+ // Optional inactivity policy. Replace undefined with your timeout in seconds
193
+ // to enable it, or remove this step if your experiment has no idle lifetime.
194
+ validateRefreshTokenIdleTimeout(refreshTokenInfo, undefined);
195
+
196
+ // online refresh token(sessionId を持つ RT)は、束縛先のログインセッションが
197
+ // 生きている間だけ使える。ログアウト・別ユーザーでの再ログインでセッションが
198
+ // 消えれば invalid_grant になる。offline_access が付与された RT は sessionId を
199
+ // 持たないため、このステップを素通りしてログアウト後も使い続けられる。
200
+ await validateRefreshTokenSession(refreshTokenInfo, authenticationSessionResolver);
201
+
202
+ // RFC 6749 §6: requested scope may only narrow the original grant.
203
+ const effectiveScope = validateRefreshTokenScope(params.scope, refreshTokenInfo.scope);
204
+
205
+ validatedRequest = buildValidatedRefreshTokenRequest(
206
+ refreshTokenInfo,
207
+ authenticatedClientId,
208
+ effectiveScope,
209
+ );
210
+ } else {
211
+ // Resolve the presented authorization code and retain the non-optional code.
212
+ const { code, authorizationCode } = await resolveAuthorizationCode(
213
+ params,
214
+ authorizationCodeResolver,
215
+ );
216
+
217
+ // OAuth 2.1 §4.1.2: reject reuse and revoke tokens from the compromised grant.
218
+ await validateAuthorizationCodeUnused(authorizationCode, authorizationCodeResolver);
219
+
220
+ // Bind the authorization code to the authenticated client and its lifetime.
221
+ validateAuthorizationCodeClient(authorizationCode, authenticatedClientId);
222
+ validateAuthorizationCodeExpiration(authorizationCode);
223
+
224
+ // OIDC Core 1.0 §3.1.3.2: bind the token request redirect_uri.
225
+ validateAuthorizationCodeRedirectUri(authorizationCode, params.redirect_uri);
226
+
227
+ // RFC 7636: validate the S256 verifier when the code carries a PKCE binding.
228
+ const codeVerified = await verifyAuthorizationCodePkce(
229
+ authorizationCode,
230
+ params.code_verifier,
231
+ );
232
+
233
+ // Mark used (do not physically delete) so a later replay remains detectable.
234
+ await consumeAuthorizationCode(code, authorizationCodeResolver);
235
+
236
+ validatedRequest = buildValidatedAuthorizationCodeRequest(
237
+ code,
238
+ authorizationCode,
239
+ authenticatedClientId,
240
+ codeVerified,
241
+ );
242
+ }
243
+ `
244
+ : ` // Grant-specific validation (authorization_code only in this configuration).
245
+ // Every security rule remains an independent customization point.
246
+ const { code, authorizationCode } = await resolveAuthorizationCode(
247
+ params,
248
+ authorizationCodeResolver,
249
+ );
250
+ await validateAuthorizationCodeUnused(authorizationCode, authorizationCodeResolver);
251
+ validateAuthorizationCodeClient(authorizationCode, authenticatedClientId);
252
+ validateAuthorizationCodeExpiration(authorizationCode);
253
+ validateAuthorizationCodeRedirectUri(authorizationCode, params.redirect_uri);
254
+ const codeVerified = await verifyAuthorizationCodePkce(
255
+ authorizationCode,
256
+ params.code_verifier,
257
+ );
258
+ await consumeAuthorizationCode(code, authorizationCodeResolver);
259
+ // The cast widens the result back to the ValidatedTokenRequest union: TypeScript
260
+ // narrows a const to its initializer type, which would make the shared
261
+ // refresh_token branches below unreachable (never) even though they compile.
262
+ const validatedRequest = buildValidatedAuthorizationCodeRequest(
263
+ code,
264
+ authorizationCode,
265
+ authenticatedClientId,
266
+ codeVerified,
267
+ ) as ValidatedTokenRequest;
268
+ `;
269
+ const refreshIssuanceDecision = refresh
270
+ ? ` // --- Refresh Token を発行するかの判定 -------------------------------------
271
+ //
272
+ // RFC 7591 §2 / OIDC Dynamic Client Registration 1.0 §2: grant_types の既定は
273
+ // ["authorization_code"]。refresh_token を登録していないクライアントへ RT を渡しても、
274
+ // 次に grant_type=refresh_token を出した瞬間 validateClientGrantType が
275
+ // unauthorized_client で拒否する。一度も使えない長期資格情報を保存させるだけなので
276
+ // (RFC 9700 §4.14)、登録が無ければ発行しない。
277
+ const clientAllowsRefreshGrant = clientAllowsRefreshTokenGrant(tokenClient);
278
+
279
+ // RFC 6749 §6 / OIDC Core 1.0 §11: refresh 時の scope 縮小は当該リクエストの access token /
280
+ // ID Token の権限縮小として扱い、refresh token rotation の可否とは切り離す。rotation 可否は
281
+ // 「元の grant が offline_access を持っていたか」で判断する。
282
+ // - authorization_code grant: 今回付与された scope に offline_access があるか。
283
+ // - refresh_token grant: 元 refresh token の grant が offline_access を持っていたか
284
+ // (validatedRequest.hadOfflineAccess)。縮小後 scope から offline_access を落としても
285
+ // 元 grant の権限は失われないため rotation を継続する。
286
+ const grantHasOfflineAccess =
287
+ clientAllowsRefreshGrant &&
288
+ (validatedRequest.grantType === 'refresh_token'
289
+ ? validatedRequest.hadOfflineAccess
290
+ : validatedRequest.scope.includes('offline_access'));
291
+
292
+ // online refresh token の束縛先セッション。
293
+ // OIDC Core 1.0 §11 は offline_access を「End-User が居ない(not logged in)ときにも
294
+ // 使える Refresh Token」と定義したうえで、Refresh Token の利用がその用途に限られない
295
+ // ことも明示している("The Authorization Server MAY grant Refresh Tokens in other
296
+ // contexts")。この OP はその other contexts を online refresh token として実装し、
297
+ // ログインセッションへ束縛する。offline_access がある grant は束縛しない。
298
+ // - authorization_code grant: 認可コードが持つ sessionId(ログイン時に確立したもの)。
299
+ // - refresh_token grant: 元 RT の束縛をそのまま引き継ぎ、rotation で外れないようにする。
300
+ const boundSessionId = grantHasOfflineAccess ? undefined : validatedRequest.sessionId;
301
+
302
+ // 束縛先が分からなければ online refresh token は発行しない。ブラウザセッションを
303
+ // 持たない経路(device authorization grant)が該当する。ログアウトで止まる保証を
304
+ // 付けられない RT を配らないための fail-closed。
305
+ const issueRefreshToken =
306
+ clientAllowsRefreshGrant &&
307
+ (grantHasOfflineAccess ||
308
+ (config.onlineRefreshTokenEnabled && boundSessionId !== undefined));
309
+
310
+ `
311
+ : '';
312
+ const refreshTokenValue = refresh
313
+ ? 'issueRefreshToken ? generateRandomString(32) : undefined'
314
+ : 'undefined /* the refresh_token feature is disabled: never issue one */';
315
+ const refreshTokenPersistence = refresh
316
+ ? ` // Store the new refresh token for rotation (OAuth 2.1 §4.3.1). grantId /
317
+ // audience / authTime / nonce / acr / amr / azp propagate through rotations,
318
+ // so descendants can be revoked on code reuse, the audience never expands,
319
+ // and refresh で再発行する ID Token は OIDC Core 1.0 §12.1 に従い初回認証時の値を保持する。
320
+ if (tokenResponse.refresh_token) {
321
+ // authTime はここで必ず確定する: authorization_code 経由は authCode.authTime、
322
+ // refresh_token 経由は validatedRequest.authTime(前段で代入済み)。
323
+ const rtAuthTime = authTime;
324
+ if (rtAuthTime === undefined) {
325
+ throw new TokenError(
326
+ TokenErrorCode.InvalidGrant,
327
+ 'authTime is required to issue a refresh token',
328
+ );
329
+ }
330
+ // OAuth 2.1 §6.1: refresh token は initial issuance からの absolute lifetime のみで失効する。
331
+ // rotation を跨いで originalIssuedAt を引き継ぎ、expiresAt はそこからの絶対的な期限で固定する。
332
+ // sliding expiry は持たないため、リフレッシュを繰り返しても失効時刻は前に進まず、
333
+ // 漏洩 RT の長期 abuse を防ぐ。
334
+ const originalIssuedAt =
335
+ validatedRequest.grantType === 'refresh_token'
336
+ ? validatedRequest.originalIssuedAt
337
+ : issuedAt;
338
+ // RFC 6749 §6: 縮小後 scope(validatedRequest.scope)から offline_access が落ちても、
339
+ // grant が offline_access を持つ限り次回以降の rotation を継続できるよう、永続化する
340
+ // refresh token の scope には offline_access を保持する。access token は
341
+ // validatedRequest.scope をそのまま使うため、当該リクエストの権限は縮小されたままになる。
342
+ const refreshTokenScope =
343
+ grantHasOfflineAccess && !validatedRequest.scope.includes('offline_access')
344
+ ? [...validatedRequest.scope, 'offline_access']
345
+ : validatedRequest.scope;
346
+ await refreshTokenStore.set(tokenResponse.refresh_token, {
347
+ subject,
348
+ clientId: validatedRequest.clientId,
349
+ scope: refreshTokenScope,
350
+ expiresAt: originalIssuedAt + config.refreshTokenAbsoluteLifetime,
351
+ originalIssuedAt,
352
+ used: false,
353
+ grantId: validatedRequest.grantId,
354
+ iat: issuedAt,
355
+ issuer: config.issuer,
356
+ audience: effectiveAudience,
357
+ authTime: rtAuthTime,
358
+ nonce,
359
+ // OIDC Core 1.0 §12.1: refresh で再発行する ID Token は初回認証時の acr / amr を保持する。
360
+ acr: validatedRequest.grantType === 'refresh_token' ? validatedRequest.acr : resolvedAcr,
361
+ amr: validatedRequest.grantType === 'refresh_token' ? validatedRequest.amr : resolvedAmr,
362
+ azp: validatedRequest.grantType === 'refresh_token' ? validatedRequest.azp : undefined,
363
+ // online refresh token の束縛。undefined なら offline refresh token として
364
+ // セッションから独立し、ログアウト後も使える。
365
+ sessionId: boundSessionId,
366
+ });
367
+ }
368
+
369
+ // OAuth 2.1 §4.3.1: ローテーションは新トークン保存成功後に旧 RT を失効する。
370
+ // 失敗時にユーザーがリフレッシュ不能になることを防ぐため、必ずこの順序にする。
371
+ if (validatedRequest.grantType === 'refresh_token' && params.refresh_token) {
372
+ await refreshTokenResolver.revokeRefreshToken(params.refresh_token);
373
+ }
374
+
375
+ `
376
+ : '';
377
+ return `/**
378
+ * Token Endpoint (OIDC Core 1.0 §3.1.3).
379
+ *
380
+ * Authenticates the client, validates the grant it presents and answers with
381
+ * the token response: authorization_code and refresh_token here.${experimentalGrantDoc}
382
+ */
383
+ import {
384
+ ${coreImports.map((name) => ` ${name},`).join('\n')}
385
+ } from '${corePkg}';${experimentalImportBlock}
386
+ import type { RegisteredClient } from '../_oidc-provider/config';
387
+ import {
388
+ accessTokenIssuer,
389
+ acrResolver,
390
+ clientResolver,
391
+ config,
392
+ loadSigningKeys,
393
+ resolvers,
394
+ selectIdTokenSigningKey,
395
+ stores,
396
+ } from '../_oidc-provider/provider';
397
+ import {
398
+ clientCors,
399
+ corsPreflight,
400
+ isFormUrlEncoded,
401
+ noStoreJson,
402
+ oauthError,
403
+ signingKeysUnavailable,
404
+ uniqueParams,
405
+ withCors,
406
+ } from '../_oidc-provider/http';
407
+
408
+ export const dynamic = 'force-dynamic';
409
+ export const runtime = 'nodejs';
410
+
411
+ const { ${resolverNames.join(', ')} } = resolvers;
412
+ const { ${storeNames.join(', ')} } = stores;
413
+
414
+ export async function POST(request: Request): Promise<Response> {
415
+ return withCors(request, clientCors, await token(request));
416
+ }
417
+
418
+ export function OPTIONS(request: Request): Response {
419
+ return corsPreflight(request, clientCors);
420
+ }
421
+
422
+ async function token(request: Request): Promise<Response> {
423
+ const keys = await loadSigningKeys().catch(() => null);
424
+ if (!keys) return signingKeysUnavailable();
425
+
426
+ // RFC 6749 §4.1.3 / OIDC Core 1.0 §3.1.3.1: the Token Request body MUST be
427
+ // application/x-www-form-urlencoded; anything else is rejected unparsed.
428
+ if (!isFormUrlEncoded(request)) {
429
+ return oauthError('invalid_request', 'Token requests must use application/x-www-form-urlencoded');
430
+ }
431
+
432
+ // RFC 6749 §3.2: token endpoint request parameters MUST NOT be repeated.
433
+ const { params: rawParams, duplicateKey } = uniqueParams(new URLSearchParams(await request.text()));
434
+ if (duplicateKey !== undefined) {
435
+ return oauthError('invalid_request', \`Parameter "\${duplicateKey}" must not be repeated\`);
436
+ }
437
+ if (!isTokenRequestParams(rawParams)) {
438
+ return oauthError('invalid_request', 'Missing required parameter: grant_type');
439
+ }
440
+ const params = rawParams;
441
+
442
+ try {
443
+ // --- Client authentication pipeline -------------------------------------
444
+ // OAuth 2.1 §2.3 / OIDC Core 1.0 §9: client_secret_basic / client_secret_post.
445
+ // Each step below is an independent core function, called in the same order
446
+ // as core's authenticateClient(). Replace verifyClientSecret with your own
447
+ // assertion check (e.g. private_key_jwt) without touching the rest.
448
+
449
+ // Read the presented credentials and which method was actually used.
450
+ const presentedCredentials = extractClientCredentials({
451
+ params,
452
+ authorizationHeader: request.headers.get('Authorization') ?? '',
453
+ });
454
+
455
+ // RFC 6749 §5.2: the presented client_id must resolve to a registered client.
456
+ const tokenClient = await resolveAuthenticatedTokenClient(
457
+ presentedCredentials.clientId,
458
+ clientResolver,
459
+ );
460
+
461
+ // OIDC Core 1.0 §9: the method used must match the registered
462
+ // token_endpoint_auth_method (blocks auth method downgrade / public-client mixups).
463
+ validateClientAuthMethod(tokenClient, presentedCredentials);
464
+
465
+ // OAuth 2.1 §7.4.1: constant-time client_secret comparison.
466
+ await verifyClientSecret(tokenClient, presentedCredentials.clientSecret);
467
+
468
+ const authenticatedClientId = presentedCredentials.clientId;
469
+ ${experimentalDispatchBlock}
470
+ // --- Token request validation pipeline --------------------------------
471
+ // Each step below is an independent core function, called in the same order
472
+ // as core's validateTokenRequest(). Delete a call to drop that validation,
473
+ // or insert your own logic between steps.
474
+
475
+ ${grantTypeSupportedStep}
476
+ // RFC 6749 §5.2: per-client grant_type authorization (unauthorized_client).
477
+ validateClientGrantType(tokenClient, grantType);
478
+
479
+ ${grantValidationStep}
480
+ // T-022: the ID Token is signed with the registered ID Token key whose alg
481
+ // matches the client's id_token_signed_response_alg (RS256 by default).
482
+ const idTokenAlg = (tokenClient as RegisteredClient).idTokenSignedResponseAlg;
483
+ const idTokenKey = selectIdTokenSigningKey(keys, idTokenAlg);
484
+ if (!idTokenKey) {
485
+ return oauthError(
486
+ 'server_error',
487
+ \`No ID Token signing key registered for alg "\${idTokenAlg ?? 'RS256'}"\`,
488
+ 500,
489
+ );
490
+ }
491
+
492
+ let subject: string;
493
+ let authTime: number | undefined;
494
+ let nonce: string | undefined;
495
+
496
+ if (validatedRequest.grantType === 'authorization_code') {
497
+ // The store holds what the consent step stored: createAuthorizationCode()'s
498
+ // record, which also carries the authenticated subject and auth_time.
499
+ const authCode = (await authCodeStore.get(validatedRequest.code)) as
500
+ | AuthorizationCodeData
501
+ | undefined;
502
+ if (!authCode?.subject || !authCode.authTime) {
503
+ throw new TokenError(
504
+ TokenErrorCode.InvalidGrant,
505
+ 'Authorization code missing required subject context',
506
+ );
507
+ }
508
+ subject = authCode.subject;
509
+ authTime = authCode.authTime;
510
+ nonce = validatedRequest.nonce;
511
+ } else {
512
+ // refresh_token grant.
513
+ // OIDC Core 1.0 §12.2: the re-issued ID Token retains iss/sub/aud/exp/iat/
514
+ // auth_time/azp/acr/amr — nonce is NOT in that list. nonce binds an
515
+ // Authentication Request to its ID Token (§2); a refresh has no such
516
+ // request, so carrying the old nonce adds no replay protection. Major OPs
517
+ // (Google, Auth0) omit it on refresh, and so does this one. auth_time is
518
+ // still preserved per §12.1.
519
+ subject = validatedRequest.subject;
520
+ authTime = validatedRequest.authTime;
521
+ nonce = undefined;
522
+ }
523
+
524
+ // アクセストークンの audience を決定する(合成ポリシーは core の buildAccessTokenAudience に集約)。
525
+ // RFC 9068 §3: JWT access token の aud は非空でなければならない。
526
+ // このアクセストークンは常に OP 自身の UserInfo エンドポイントで使用できるため、UserInfo
527
+ // エンドポイント(discovery が広告する userinfo_endpoint と同じ URL)を aud の恒久メンバとして
528
+ // 必ず含める。resource 指定(validatedRequest.audience)があれば末尾に追加し、UserInfo
529
+ // エンドポイントを取り除くことはしない。重複は除去される。
530
+ // refresh では保存済み aud(既に UserInfo を含む)を引き継ぐため、再計算しても同一集合になる。
531
+ const effectiveAudience = buildAccessTokenAudience({
532
+ userInfoEndpoint: \`\${config.issuer}/userinfo\`,
533
+ requested: validatedRequest.audience,
534
+ issuer: config.issuer,
535
+ });
536
+
537
+ ${refreshIssuanceDecision} // --- Token response pipeline --------------------------------------------
538
+ // Each step below is an independent core function, called in the same order
539
+ // as core's generateTokenResponse(). Add your own ID Token claims by editing
540
+ // idTokenPayload before it is signed.
541
+
542
+ // One timestamp for the whole response so the issued tokens and the stored
543
+ // token metadata agree on iat / exp.
544
+ const issuedAt = Math.floor(Date.now() / 1000);
545
+
546
+ // RFC 9068 §2.2: iss / sub / aud / exp / iat / scope / client_id.
547
+ // Add access token claims here before the payload is signed.
548
+ const accessTokenPayload = buildAccessTokenPayload({
549
+ issuer: config.issuer,
550
+ subject,
551
+ clientId: validatedRequest.clientId,
552
+ scope: validatedRequest.scope,
553
+ audience: effectiveAudience,
554
+ expiresIn: config.accessTokenExpiresIn,
555
+ issuedAt,
556
+ });
557
+
558
+ // JWT or opaque, as chosen by config.accessTokenFormat (provider.ts).
559
+ const accessToken = await accessTokenIssuer.issue({
560
+ payload: accessTokenPayload,
561
+ privateKey: keys.general.active.privateKey,
562
+ keyId: keys.general.active.keyId,
563
+ });
564
+
565
+ // OIDC Core 1.0 §12: refresh_token grant でも id_token は MAY。
566
+ // openid scope を持つ場合は §12.1 に従い初回認証時と同じ auth_time / acr / amr / azp で再発行する。
567
+ // (§12.2 は nonce を再発行 ID Token の保持クレームに挙げないため nonce は refresh では undefined)
568
+ let idToken: string | undefined;
569
+ let resolvedAcr: string | undefined = undefined;
570
+ let resolvedAmr: string[] | undefined = undefined;
571
+ if (validatedRequest.scope.includes('openid')) {
572
+ // OIDC Core 1.0 §3.1.3.6: at_hash binds the ID Token to this access token.
573
+ // The hash function follows the ID Token signing alg.
574
+ const atHash = await computeAtHash(accessToken, idTokenKey.privateKey);
575
+
576
+ // T-015: acr / amr.
577
+ // - authorization_code: ask acrResolver (provider.ts); acr_values and the
578
+ // claims request are forwarded so it can honor them.
579
+ // - refresh_token: the stored acr / amr are reused so OIDC Core 1.0 §12.1
580
+ // "preserve initial auth context" holds; the resolver is bypassed.
581
+ ({ acr: resolvedAcr, amr: resolvedAmr } = await resolveAcrAmr({
582
+ subject,
583
+ clientId: validatedRequest.clientId,
584
+ acr: validatedRequest.grantType === 'refresh_token' ? validatedRequest.acr : undefined,
585
+ amr: validatedRequest.grantType === 'refresh_token' ? validatedRequest.amr : undefined,
586
+ acrResolver: validatedRequest.grantType === 'authorization_code' ? acrResolver : undefined,
587
+ requestedAcrValues:
588
+ validatedRequest.grantType === 'authorization_code' ? validatedRequest.acrValues : undefined,
589
+ // OIDC Core 1.0 §5.5: the parsed claims request lets the resolver satisfy
590
+ // id_token member requests (e.g. acr.values).
591
+ claims: validatedRequest.grantType === 'authorization_code' ? validatedRequest.claims : undefined,
592
+ }));
593
+
594
+ const idTokenPayload = buildIdTokenPayload({
595
+ issuer: config.issuer,
596
+ subject,
597
+ clientId: validatedRequest.clientId,
598
+ scope: validatedRequest.scope,
599
+ expiresIn: config.idTokenExpiresIn,
600
+ issuedAt,
601
+ atHash,
602
+ nonce,
603
+ authTime,
604
+ acr: resolvedAcr,
605
+ amr: resolvedAmr,
606
+ });
607
+
608
+ // Add your own ID Token claims here, e.g.:
609
+ // idTokenPayload.tenant_id = await lookupTenant(subject);
610
+
611
+ idToken = await generateIdToken({
612
+ payload: idTokenPayload,
613
+ privateKey: idTokenKey.privateKey,
614
+ keyId: idTokenKey.keyId,
615
+ });
616
+ }
617
+
618
+ // OIDC Core 1.0 §3.1.3.3 / RFC 6749 §5.1: the token response body.
619
+ const tokenResponse = {
620
+ access_token: accessToken,
621
+ token_type: 'Bearer' as const,
622
+ expires_in: config.accessTokenExpiresIn,
623
+ id_token: idToken,
624
+ scope: validatedRequest.scope.join(' '),
625
+ refresh_token: ${refreshTokenValue},
626
+ };
627
+
628
+ // Store the access token for the UserInfo / Introspection / Revocation
629
+ // endpoints. iat / nbf / audience / issuer are kept so RFC 7662
630
+ // introspection can echo them; grantId ties the token to its authorization
631
+ // grant so it is revoked with its siblings on code reuse (OAuth 2.1 §4.1.2).
632
+ await accessTokenStore.set(tokenResponse.access_token, {
633
+ sub: subject,
634
+ clientId: validatedRequest.clientId,
635
+ scope: validatedRequest.scope,
636
+ expiresAt: issuedAt + config.accessTokenExpiresIn,
637
+ grantId: validatedRequest.grantId,
638
+ iat: issuedAt,
639
+ // RFC 7519 §4.1.5 / RFC 7662 §2.2: persist nbf (= iat) for JWT and opaque
640
+ // tokens alike so introspection reports a not-yet-valid token inactive.
641
+ nbf: issuedAt,
642
+ audience: effectiveAudience,
643
+ issuer: config.issuer,
644
+ // RFC 9068 §2.2 / RFC 7662 §2.2: the token identifier core minted for this
645
+ // issuance. It also keeps two same-second issuances distinct (RS256 is
646
+ // deterministic), so this store key never collides across grants.
647
+ jti: accessTokenPayload.jti,
648
+ // OIDC Core 1.0 §5.5: the authorization request's claims parameter, so the
649
+ // UserInfo endpoint can honor claims.userinfo members independently of scope.
650
+ claims: validatedRequest.grantType === 'authorization_code' ? validatedRequest.claims : undefined,
651
+ });
652
+
653
+ ${refreshTokenPersistence} return noStoreJson(tokenResponse);
654
+ } catch (error) {
655
+ ${experimentalErrorMapping} if (error instanceof TokenError) {
656
+ // RFC 6750 §3 / OAuth 2.1 §5.2: a 401 carries WWW-Authenticate.
657
+ return oauthError(
658
+ error.error,
659
+ error.errorDescription,
660
+ error.statusCode,
661
+ error.wwwAuthenticate ? { 'WWW-Authenticate': error.wwwAuthenticate } : undefined,
662
+ );
663
+ }
664
+ return oauthError('server_error', undefined, 500);
665
+ }
666
+ }
667
+
668
+ /** Narrows raw body params to TokenRequestParams: grant_type is required. */
669
+ function isTokenRequestParams(params: unknown): params is TokenRequestParams {
670
+ if (typeof params !== 'object' || params === null) return false;
671
+ return typeof (params as Record<string, unknown>)['grant_type'] === 'string';
672
+ }
673
+ `;
674
+ }
675
+ export function nextJsTokenExchangeGrantTemplate(corePkg) {
676
+ return `/**
677
+ * EXPERIMENTAL — OAuth 2.0 Token Exchange (RFC 8693 §2.1).
678
+ *
679
+ * Generated because the OP was created with \`--enable token-exchange\`.
680
+ ${EXPERIMENTAL_NOTICE}
681
+ *
682
+ * Known limitation: RFC 8693 §2.1 permits repeated \`resource\` / \`audience\`
683
+ * parameters, but the token endpoint rejects any repeated parameter (RFC 6749
684
+ * §3.2), so only a single value of each is supported.
685
+ */
686
+ import {
687
+ buildAccessTokenAudience,
688
+ buildAccessTokenPayload,
689
+ type TokenClientInfo,
690
+ } from '${corePkg}';
691
+ import {
692
+ buildTokenExchangeResponse,
693
+ processTokenExchangeRequest,
694
+ type ExchangedAccessTokenInfo,
695
+ } from '${EXPERIMENTAL_PACKAGE}/token-exchange';
696
+ import {
697
+ accessTokenIssuer,
698
+ config,
699
+ resolvers,
700
+ stores,
701
+ type ProviderSigningKeys,
702
+ } from '../_oidc-provider/provider';
703
+ import { noStoreJson } from '../_oidc-provider/http';
704
+
705
+ /**
706
+ * Token Exchange settings.
707
+ *
708
+ * - allowedTargets: the audience / resource values a client may ask an
709
+ * exchanged token to be issued for. Empty by default (fail safe): with an
710
+ * empty list every exchange that names a target is rejected with
711
+ * invalid_target, and only scope-narrowing / lifetime-shortening exchanges
712
+ * succeed. Add the identifiers of your downstream services here.
713
+ */
714
+ export const tokenExchangeConfig = {
715
+ allowedTargets: [] as string[],
716
+ };
717
+
718
+ /**
719
+ * Exchange the subject_token for a new access token. Errors are thrown as
720
+ * TokenExchangeError and answered by the token route.
721
+ */
722
+ export async function exchangeToken(
723
+ params: Record<string, string>,
724
+ client: TokenClientInfo,
725
+ keys: ProviderSigningKeys,
726
+ ): Promise<Response> {
727
+ // Validate the request and derive the issuing material. Each check inside is
728
+ // also exported as its own step function, so you can call them one by one
729
+ // instead and drop or replace individual rules.
730
+ const grant = await processTokenExchangeRequest({
731
+ params,
732
+ client,
733
+ accessTokenResolver: resolvers.accessTokenResolver,
734
+ allowedTargets: tokenExchangeConfig.allowedTargets,
735
+ configuredExpiresIn: config.accessTokenExpiresIn,
736
+ });
737
+
738
+ // Same aud policy as the standard grants: the UserInfo endpoint stays a
739
+ // permanent member (RFC 9068 §3), so an exchanged token still passes the
740
+ // UserInfo endpoint's audience check.
741
+ const audience = buildAccessTokenAudience({
742
+ userInfoEndpoint: \`\${config.issuer}/userinfo\`,
743
+ requested: grant.requestedAudience,
744
+ issuer: config.issuer,
745
+ });
746
+
747
+ const issuedAt = Math.floor(Date.now() / 1000);
748
+ const payload = buildAccessTokenPayload({
749
+ issuer: config.issuer,
750
+ subject: grant.subject,
751
+ clientId: grant.clientId,
752
+ scope: grant.scope,
753
+ audience,
754
+ expiresIn: grant.expiresIn,
755
+ issuedAt,
756
+ });
757
+ const accessToken = await accessTokenIssuer.issue({
758
+ payload: {
759
+ ...payload,
760
+ // RFC 8693 §4.1: a delegation exchange records the current actor in the
761
+ // act claim (chains already nested by processTokenExchangeRequest).
762
+ // Impersonation exchanges carry no act claim.
763
+ ...(grant.actor === undefined ? {} : { act: grant.actor }),
764
+ },
765
+ privateKey: keys.general.active.privateKey,
766
+ keyId: keys.general.active.keyId,
767
+ });
768
+
769
+ const metadata: ExchangedAccessTokenInfo = {
770
+ // RFC 8693 §1.1: the exchanged token acts as the same subject, but is bound
771
+ // to the client that requested the exchange.
772
+ sub: grant.subject,
773
+ clientId: grant.clientId,
774
+ scope: grant.scope,
775
+ expiresAt: issuedAt + grant.expiresIn,
776
+ // Inherit the subject token's grant so revoking the grant (e.g. on code
777
+ // reuse detection) also kills every token exchanged from it.
778
+ grantId: grant.grantId,
779
+ iat: issuedAt,
780
+ nbf: issuedAt,
781
+ audience,
782
+ issuer: config.issuer,
783
+ // RFC 9068 §2.2 / RFC 7662 §2.2: the exchanged token gets its own jti, so it
784
+ // is a distinct store record even when exchanged twice within one second.
785
+ jti: payload.jti,
786
+ // Persisting act lets a later exchange that presents THIS token as its
787
+ // subject_token pick up the chain (RFC 8693 §4.1 nesting).
788
+ ...(grant.actor === undefined ? {} : { act: grant.actor }),
789
+ // The subject token's stored claims parameter (OIDC Core 1.0 §5.5) is
790
+ // deliberately NOT inherited: an exchanged token yields scope-based claims
791
+ // only at the UserInfo endpoint.
792
+ };
793
+ await stores.accessTokenStore.set(accessToken, metadata);
794
+
795
+ // RFC 8693 §2.2.1: access_token / issued_token_type / token_type are
796
+ // REQUIRED; expires_in and scope are always included here.
797
+ return noStoreJson(buildTokenExchangeResponse({
798
+ accessToken,
799
+ expiresIn: grant.expiresIn,
800
+ scope: grant.scope,
801
+ }));
802
+ }
803
+ `;
804
+ }
805
+ export function nextJsIdJagGrantTemplate(corePkg, features = DEFAULT_FEATURES) {
806
+ const refreshSubjects = features.refreshToken;
807
+ const refreshConfigDoc = refreshSubjects
808
+ ? `
809
+ * - allowRefreshTokenSubjects: whether a refresh token this OP issued may stand
810
+ * in for the ID Token as the subject_token (draft §4.3 MAY), so a client can
811
+ * request a fresh ID-JAG after its ID Token expired without a new SSO round
812
+ * trip. Validated exactly like the standard refresh_token grant (rotation
813
+ * reuse revokes the token family; online tokens require the login session to
814
+ * be alive); the refresh token is NOT consumed. Grants without the openid
815
+ * scope are refused — their refresh token replaces no identity assertion.`
816
+ : '';
817
+ const refreshConfigField = refreshSubjects
818
+ ? `
819
+ allowRefreshTokenSubjects: true,`
820
+ : '';
821
+ const refreshSubjectArgs = refreshSubjects
822
+ ? `
823
+ ...(idJagConfig.allowRefreshTokenSubjects
824
+ ? {
825
+ refreshTokenResolver: resolvers.refreshTokenResolver,
826
+ authenticationSessionResolver: resolvers.authenticationSessionResolver,
827
+ }
828
+ : {}),`
829
+ : '';
830
+ const resolversImport = refreshSubjects ? '\n resolvers,' : '';
831
+ return `/**
832
+ * EXPERIMENTAL — Identity Assertion JWT Authorization Grant (ID-JAG), the
833
+ * Cross-App Access flow (draft-ietf-oauth-identity-assertion-authz-grant-04).
834
+ *
835
+ * Generated because the OP was created with \`--enable id-jag\`. The OP plays
836
+ * both roles:
837
+ * - as the IdP it issues ID-JAGs on the token-exchange grant (issueIdJag);
838
+ * - as a resource authorization server it redeems ID-JAGs from trusted IdPs on
839
+ * the jwt-bearer grant (redeemIdJag).
840
+ *
841
+ ${EXPERIMENTAL_NOTICE} The underlying specification is an IETF draft and may
842
+ * itself change.
843
+ */
844
+ import {
845
+ buildAccessTokenAudience,
846
+ buildAccessTokenPayload,
847
+ selectSigningKeyByAlg,
848
+ type JwkSet,
849
+ type SigningKey,
850
+ type TokenClientInfo,
851
+ } from '${corePkg}';
852
+ import {
853
+ TOKEN_TYPE_ID_TOKEN,
854
+ processIdJagIssuanceRequest,
855
+ processIdJagRedemptionRequest,
856
+ resolveIdJagActor,
857
+ type IdJagAccessTokenInfo,
858
+ type IdJagActorTokenResolver,
859
+ type IdJagTrustedIdentityProvider,
860
+ } from '${EXPERIMENTAL_PACKAGE}/id-jag';
861
+ import {
862
+ accessTokenIssuer,
863
+ config,
864
+ idTokenHintJwks,${resolversImport}
865
+ stores,
866
+ type ProviderSigningKeys,
867
+ } from '../_oidc-provider/provider';
868
+ import { noStoreJson, oauthError } from '../_oidc-provider/http';
869
+
870
+ /**
871
+ * Cross-App Access settings.
872
+ *
873
+ * Issuing side (this OP as the IdP, draft §4.3):
874
+ * - allowedAudiences: resource authorization server issuers this IdP may issue
875
+ * an ID-JAG for. Empty by default (fail safe): every issuance request is
876
+ * rejected with invalid_target until you list the peer AS issuers here.
877
+ * Adding an entry grants that cross-app connection on behalf of every user —
878
+ * there is no per-user consent screen in this flow.
879
+ * - idJagLifetimeSeconds: ID-JAG lifetime. Keep it short (draft example: 300);
880
+ * clients are expected to request a fresh one instead of holding it.
881
+ * - allowedScopes: optional cap on the scopes an ID-JAG may carry. undefined
882
+ * passes the requested scopes through (the resource AS applies its own
883
+ * policy again on redemption).${refreshConfigDoc}
884
+ * - allowActorTokens: whether an actor_token (identifying who acts on the
885
+ * subject's behalf) is accepted and recorded as the ID-JAG's act claim
886
+ * (RFC 8693 §4.1). The draft defines no normative actor processing (§9.7
887
+ * sketches extensions), so this is an opt-in extension and defaults to
888
+ * false — an actor_token is rejected until you flip it, whatever else is
889
+ * configured. Every token type identifier RFC 8693 §3 defines is accepted
890
+ * the same way; the type alone decides nothing.
891
+ * - actorTokenResolver: validates the actor_token's CONTENT (signature,
892
+ * revocation, whose token it is) — for every accepted type, this OP's own
893
+ * ID Tokens included. The library only checks the request structure and the
894
+ * shape of what you return. Return the act value ({ sub, act? }) for a valid
895
+ * token, null for an invalid one (answered with a fixed invalid_request), or
896
+ * throw IdJagError to pick the response yourself. The default below handles
897
+ * ID Tokens this OP issued to the authenticated client; extend or replace it
898
+ * to cover the other types. Clearing it rejects every actor_token.
899
+ *
900
+ * Consuming side (this OP as the resource authorization server, draft §4.4):
901
+ * - trustedIdentityProviders: the IdPs whose ID-JAGs are accepted on the
902
+ * jwt-bearer grant. Empty by default (fail safe). Keys come from the inline
903
+ * \`jwks\` when present, otherwise from \`jwksUri\` (fetched and cached below).
904
+ * Never derive the key source from the assertion itself.
905
+ */
906
+ const defaultIdJagActorTokenResolver: IdJagActorTokenResolver = async ({
907
+ actorToken,
908
+ actorTokenType,
909
+ clientId,
910
+ issuer,
911
+ jwks,
912
+ }) =>
913
+ actorTokenType === TOKEN_TYPE_ID_TOKEN
914
+ ? resolveIdJagActor({ actorToken, issuer, clientId, jwks })
915
+ : null;
916
+
917
+ export const idJagConfig = {
918
+ allowedAudiences: [] as string[],
919
+ idJagLifetimeSeconds: 300,
920
+ allowedScopes: undefined as string[] | undefined,${refreshConfigField}
921
+ allowActorTokens: false,
922
+ actorTokenResolver: defaultIdJagActorTokenResolver as IdJagActorTokenResolver | undefined,
923
+ trustedIdentityProviders: [] as Array<{ issuer: string; jwksUri?: string; jwks?: JwkSet }>,
924
+ };
925
+
926
+ /**
927
+ * Issue an ID-JAG (draft §4.3): the subject_token must be an ID Token this OP
928
+ * issued to the authenticated client, and the answer is a signed grant JWT for
929
+ * the resource authorization server named by \`audience\` — not an access token
930
+ * (the response carries token_type N_A). Errors are thrown as IdJagError and
931
+ * answered by the token route.
932
+ */
933
+ export async function issueIdJag(
934
+ params: Record<string, string>,
935
+ client: TokenClientInfo,
936
+ keys: ProviderSigningKeys,
937
+ ): Promise<Response> {
938
+ // Signed with a registered RS256 key so the peer AS can verify it against
939
+ // this OP's JWKS endpoint (same key-selection contract as JARM: RS256 is
940
+ // pinned, the active key may be a different alg).
941
+ let signingKey: SigningKey;
942
+ try {
943
+ signingKey = selectSigningKeyByAlg(keys.general.registered, 'RS256');
944
+ } catch {
945
+ return oauthError('server_error', 'No RS256 signing key registered for ID-JAG issuance', 500);
946
+ }
947
+
948
+ const response = await processIdJagIssuanceRequest({
949
+ params,
950
+ client,
951
+ issuer: config.issuer,
952
+ // The subject_token is verified against the keys id_token_hint uses (this
953
+ // OP's own ID Token keys); draft §4.3.3 requires its audience to be the
954
+ // authenticated client, which processIdJagIssuanceRequest checks.
955
+ jwks: await idTokenHintJwks(keys),
956
+ signingKey,
957
+ allowedAudiences: idJagConfig.allowedAudiences,
958
+ allowedScopes: idJagConfig.allowedScopes,
959
+ lifetimeSeconds: idJagConfig.idJagLifetimeSeconds,
960
+ // Extension (draft §9.7): when enabled, an actor_token is recorded as the
961
+ // ID-JAG's act claim. Every accepted token type goes through the same
962
+ // resolver, which owns the content validation.
963
+ allowActorTokens: idJagConfig.allowActorTokens,
964
+ ...(idJagConfig.actorTokenResolver === undefined
965
+ ? {}
966
+ : { actorTokenResolver: idJagConfig.actorTokenResolver }),${refreshSubjectArgs}
967
+ });
968
+
969
+ // The ID-JAG itself is not persisted: it is a self-contained signed grant the
970
+ // peer AS verifies by signature and exp.
971
+ return noStoreJson(response);
972
+ }
973
+
974
+ /**
975
+ * Redeem an ID-JAG on the jwt-bearer grant (RFC 7523 §2.1 / draft §4.4). The
976
+ * assertion must be issued by one of idJagConfig.trustedIdentityProviders for
977
+ * THIS issuer and the authenticated client; this OP then issues its own access
978
+ * token. No ID Token (this is not an OIDC authentication) and no refresh token
979
+ * (draft §4.4.3 SHOULD NOT — the still-valid ID-JAG can be presented again).
980
+ */
981
+ export async function redeemIdJag(
982
+ params: Record<string, string>,
983
+ client: TokenClientInfo,
984
+ keys: ProviderSigningKeys,
985
+ ): Promise<Response> {
986
+ const grant = await processIdJagRedemptionRequest({
987
+ params,
988
+ client,
989
+ issuer: config.issuer,
990
+ identityProviders: await resolveTrustedIdentityProviders(),
991
+ configuredExpiresIn: config.accessTokenExpiresIn,
992
+ });
993
+
994
+ // Same aud policy as the standard grants: the UserInfo endpoint stays a
995
+ // permanent member (RFC 9068 §3); the ID-JAG's resource claim (RFC 8707)
996
+ // contributes the requested resources.
997
+ const audience = buildAccessTokenAudience({
998
+ userInfoEndpoint: \`\${config.issuer}/userinfo\`,
999
+ requested: grant.requestedResources,
1000
+ issuer: config.issuer,
1001
+ });
1002
+
1003
+ const issuedAt = Math.floor(Date.now() / 1000);
1004
+ const payload = buildAccessTokenPayload({
1005
+ issuer: config.issuer,
1006
+ subject: grant.subject,
1007
+ clientId: grant.clientId,
1008
+ scope: grant.scope,
1009
+ audience,
1010
+ expiresIn: grant.expiresIn,
1011
+ issuedAt,
1012
+ });
1013
+ const accessToken = await accessTokenIssuer.issue({
1014
+ payload: {
1015
+ ...payload,
1016
+ // RFC 8693 §4.1: an act claim carried by the ID-JAG is preserved, so
1017
+ // downstream services still see WHO acts on the subject's behalf
1018
+ // (dropping it would silently turn the delegation into impersonation).
1019
+ ...(grant.actor === undefined ? {} : { act: grant.actor }),
1020
+ },
1021
+ privateKey: keys.general.active.privateKey,
1022
+ keyId: keys.general.active.keyId,
1023
+ });
1024
+
1025
+ const metadata: IdJagAccessTokenInfo = {
1026
+ // draft §4.4.1: the ID-JAG's sub is used as the local subject directly
1027
+ // (subject resolution by identical sub; JIT provisioning is out of scope).
1028
+ sub: grant.subject,
1029
+ clientId: grant.clientId,
1030
+ scope: grant.scope,
1031
+ expiresAt: issuedAt + grant.expiresIn,
1032
+ // Each redemption is its own grant: revoking one issued token must not
1033
+ // affect tokens from other redemptions of the same (re-presentable) ID-JAG,
1034
+ // so the payload's own jti doubles as the grant id.
1035
+ grantId: payload.jti,
1036
+ iat: issuedAt,
1037
+ nbf: issuedAt,
1038
+ audience,
1039
+ issuer: config.issuer,
1040
+ jti: payload.jti,
1041
+ // The actor is persisted too, so opaque-token introspection and store-based
1042
+ // tooling can surface it just like the JWT claim.
1043
+ ...(grant.actor === undefined ? {} : { act: grant.actor }),
1044
+ };
1045
+ await stores.accessTokenStore.set(accessToken, metadata);
1046
+
1047
+ return noStoreJson({
1048
+ access_token: accessToken,
1049
+ token_type: 'Bearer' as const,
1050
+ expires_in: grant.expiresIn,
1051
+ scope: grant.scope.join(' '),
1052
+ });
1053
+ }
1054
+
1055
+ /**
1056
+ * jwks_uri cache for trusted identity providers. A fetched JWKS is reused for
1057
+ * 300 seconds, so a key rotation at the IdP can take up to that long to be
1058
+ * picked up. The fetch target comes exclusively from idJagConfig above — never
1059
+ * from request or assertion content — which keeps this endpoint SSRF-free.
1060
+ */
1061
+ const idJagJwksCache = new Map<string, { jwks: JwkSet; expiresAt: number }>();
1062
+ const ID_JAG_JWKS_CACHE_TTL_MS = 300_000;
1063
+
1064
+ async function resolveTrustedIdentityProviders(): Promise<IdJagTrustedIdentityProvider[]> {
1065
+ const resolved: IdJagTrustedIdentityProvider[] = [];
1066
+ for (const entry of idJagConfig.trustedIdentityProviders) {
1067
+ if (entry.jwks !== undefined) {
1068
+ resolved.push({ issuer: entry.issuer, jwks: entry.jwks });
1069
+ continue;
1070
+ }
1071
+ if (entry.jwksUri === undefined) {
1072
+ // An entry with neither jwks nor jwksUri can never verify anything; skip
1073
+ // it so the assertion is answered with the fixed untrusted description.
1074
+ continue;
1075
+ }
1076
+ const cached = idJagJwksCache.get(entry.jwksUri);
1077
+ if (cached !== undefined && cached.expiresAt > Date.now()) {
1078
+ resolved.push({ issuer: entry.issuer, jwks: cached.jwks });
1079
+ continue;
1080
+ }
1081
+ // A failed fetch propagates: the token route turns it into server_error,
1082
+ // which is honest — the assertion was never evaluated, so invalid_grant
1083
+ // would wrongly blame the client for an outage on this side.
1084
+ const response = await fetch(entry.jwksUri);
1085
+ if (!response.ok) {
1086
+ throw new Error(\`Fetching the JWKS of trusted IdP \${entry.issuer} failed with status \${response.status}\`);
1087
+ }
1088
+ const jwks = (await response.json()) as JwkSet;
1089
+ idJagJwksCache.set(entry.jwksUri, { jwks, expiresAt: Date.now() + ID_JAG_JWKS_CACHE_TTL_MS });
1090
+ resolved.push({ issuer: entry.issuer, jwks });
1091
+ }
1092
+ return resolved;
1093
+ }
1094
+ `;
1095
+ }
1096
+ function approvedGrantTokens(input) {
1097
+ return ` // T-022: the ID Token follows the same key-selection rule as the standard
1098
+ // grants — the registered ID Token key whose alg matches the client's
1099
+ // id_token_signed_response_alg, not the general-purpose active key.
1100
+ const idTokenAlg = (client as RegisteredClient).idTokenSignedResponseAlg;
1101
+ const idTokenKey = selectIdTokenSigningKey(keys, idTokenAlg);
1102
+ if (!idTokenKey) {
1103
+ return oauthError(
1104
+ 'server_error',
1105
+ \`No ID Token signing key registered for alg "\${idTokenAlg ?? 'RS256'}"\`,
1106
+ 500,
1107
+ );
1108
+ }
1109
+
1110
+ // Same aud policy as the standard grants: the UserInfo endpoint stays a
1111
+ // permanent member (RFC 9068 §3). ${input.audienceNote}
1112
+ const audience = buildAccessTokenAudience({
1113
+ userInfoEndpoint: \`\${config.issuer}/userinfo\`,
1114
+ issuer: config.issuer,
1115
+ });
1116
+
1117
+ const issuedAt = Math.floor(Date.now() / 1000);
1118
+ const accessTokenPayload = buildAccessTokenPayload({
1119
+ issuer: config.issuer,
1120
+ subject: ${input.grant}.subject,
1121
+ clientId: ${input.grant}.clientId,
1122
+ scope: ${input.grant}.scope,
1123
+ audience,
1124
+ expiresIn: config.accessTokenExpiresIn,
1125
+ issuedAt,
1126
+ });
1127
+ const accessToken = await accessTokenIssuer.issue({
1128
+ payload: accessTokenPayload,
1129
+ privateKey: keys.general.active.privateKey,
1130
+ keyId: keys.general.active.keyId,
1131
+ });
1132
+
1133
+ // ${input.idTokenNote}
1134
+ const { acr, amr } = await resolveAcrAmr({
1135
+ subject: ${input.grant}.subject,
1136
+ clientId: ${input.grant}.clientId,
1137
+ acrResolver,
1138
+ });
1139
+ const idToken = await generateIdToken({
1140
+ payload: buildIdTokenPayload({
1141
+ issuer: config.issuer,
1142
+ subject: ${input.grant}.subject,
1143
+ clientId: ${input.grant}.clientId,
1144
+ scope: ${input.grant}.scope,
1145
+ expiresIn: config.idTokenExpiresIn,
1146
+ issuedAt,
1147
+ atHash: await computeAtHash(accessToken, idTokenKey.privateKey),
1148
+ authTime: ${input.grant}.authTime,
1149
+ acr,
1150
+ amr,
1151
+ }),
1152
+ privateKey: idTokenKey.privateKey,
1153
+ keyId: idTokenKey.keyId,
1154
+ });
1155
+
1156
+ await stores.accessTokenStore.set(accessToken, {
1157
+ sub: ${input.grant}.subject,
1158
+ clientId: ${input.grant}.clientId,
1159
+ scope: ${input.grant}.scope,
1160
+ expiresAt: issuedAt + config.accessTokenExpiresIn,
1161
+ // ${input.grantIdNote}
1162
+ grantId: ${input.grant}.grantId,
1163
+ iat: issuedAt,
1164
+ nbf: issuedAt,
1165
+ audience,
1166
+ issuer: config.issuer,
1167
+ jti: accessTokenPayload.jti,
1168
+ });
1169
+
1170
+ ${input.refreshTokens ? ` // OIDC Core 1.0 §11: offline_access survived the request endpoint's policy
1171
+ // check only if this client may hold refresh tokens, and ${input.offlineNote}
1172
+ // IS the explicit consent §11 asks for. Nothing further to gate on here.
1173
+ const refreshToken = ${input.grant}.scope.includes('offline_access')
1174
+ ? generateRandomString(32)
1175
+ : undefined;
1176
+ if (refreshToken) {
1177
+ await stores.refreshTokenStore.set(refreshToken, {
1178
+ subject: ${input.grant}.subject,
1179
+ clientId: ${input.grant}.clientId,
1180
+ scope: ${input.grant}.scope,
1181
+ // OAuth 2.1 §6.1: absolute lifetime from initial issuance; rotations
1182
+ // inherit originalIssuedAt so the deadline never slides forward.
1183
+ expiresAt: issuedAt + config.refreshTokenAbsoluteLifetime,
1184
+ originalIssuedAt: issuedAt,
1185
+ used: false,
1186
+ grantId: ${input.grant}.grantId,
1187
+ iat: issuedAt,
1188
+ issuer: config.issuer,
1189
+ audience,
1190
+ authTime: ${input.grant}.authTime,
1191
+ // ${input.nonceNote}
1192
+ nonce: undefined,
1193
+ acr,
1194
+ amr,
1195
+ azp: undefined,
1196
+ });
1197
+ }
1198
+
1199
+ ` : ''} return noStoreJson({
1200
+ access_token: accessToken,
1201
+ token_type: 'Bearer' as const,
1202
+ expires_in: config.accessTokenExpiresIn,
1203
+ id_token: idToken,
1204
+ scope: ${input.grant}.scope.join(' '),${input.refreshTokens ? `
1205
+ refresh_token: refreshToken,` : ''}
1206
+ });`;
1207
+ }
1208
+ export function nextJsDeviceCodeGrantTemplate(corePkg, features = DEFAULT_FEATURES) {
1209
+ const randomStringImport = features.refreshToken ? '\n generateRandomString,' : '';
1210
+ return `/**
1211
+ * EXPERIMENTAL — the device_code grant of the OAuth 2.0 Device Authorization
1212
+ * Grant (RFC 8628 §3.4 / §3.5).
1213
+ *
1214
+ * Generated because the OP was created with \`--enable device-authorization-grant\`.
1215
+ ${EXPERIMENTAL_NOTICE}
1216
+ */
1217
+ import {
1218
+ buildAccessTokenAudience,
1219
+ buildAccessTokenPayload,
1220
+ buildIdTokenPayload,
1221
+ computeAtHash,
1222
+ generateIdToken,${randomStringImport}
1223
+ resolveAcrAmr,
1224
+ type TokenClientInfo,
1225
+ } from '${corePkg}';
1226
+ import { processDeviceCodeGrant } from '${EXPERIMENTAL_PACKAGE}/device-authorization-grant';
1227
+ import type { RegisteredClient } from '../_oidc-provider/config';
1228
+ import {
1229
+ accessTokenIssuer,
1230
+ acrResolver,
1231
+ config,
1232
+ deviceAuthorizationStore,
1233
+ selectIdTokenSigningKey,
1234
+ stores,
1235
+ type ProviderSigningKeys,
1236
+ } from '../_oidc-provider/provider';
1237
+ import { noStoreJson, oauthError } from '../_oidc-provider/http';
1238
+
1239
+ /**
1240
+ * Redeem an approved device authorization for tokens. Every state but
1241
+ * "approved" is thrown as DeviceAuthorizationError — authorization_pending /
1242
+ * slow_down / access_denied / expired_token, plus invalid_request /
1243
+ * invalid_grant / unauthorized_client from §3.4 — and answered by the token
1244
+ * route.
1245
+ */
1246
+ export async function redeemDeviceCode(
1247
+ params: Record<string, string>,
1248
+ client: TokenClientInfo,
1249
+ keys: ProviderSigningKeys,
1250
+ ): Promise<Response> {
1251
+ const deviceGrant = await processDeviceCodeGrant({
1252
+ params,
1253
+ client,
1254
+ store: deviceAuthorizationStore,
1255
+ });
1256
+
1257
+ ${approvedGrantTokens({
1258
+ grant: 'deviceGrant',
1259
+ refreshTokens: features.refreshToken,
1260
+ audienceNote: 'RFC 8628 has no resource parameter, so nothing else is\n // requested.',
1261
+ idTokenNote: `The device authorization endpoint requires the openid scope, so an ID Token
1262
+ // is always issued. It carries no nonce (RFC 8628 defines no such parameter,
1263
+ // and OIDC Core 1.0 §2 only requires nonce when the authentication request
1264
+ // carried one) and no c_hash (there is no code).`,
1265
+ grantIdNote: 'Inherit the grantId minted at approval so revoking the grant kills\n // every token issued from this device authorization.',
1266
+ offlineNote: 'the approval screen the user\n // just went through',
1267
+ nonceNote: 'RFC 8628 has no nonce parameter, so the re-issued ID Token has none\n // to preserve either.',
1268
+ })}
1269
+ }
1270
+ `;
1271
+ }
1272
+ export function nextJsCibaGrantTemplate(corePkg, features = DEFAULT_FEATURES) {
1273
+ const randomStringImport = features.refreshToken ? '\n generateRandomString,' : '';
1274
+ return `/**
1275
+ * EXPERIMENTAL — the CIBA grant (OpenID Connect Client-Initiated Backchannel
1276
+ * Authentication Core 1.0 §10.1, poll mode).
1277
+ *
1278
+ * Generated because the OP was created with \`--enable ciba\`.
1279
+ ${EXPERIMENTAL_NOTICE}
1280
+ */
1281
+ import {
1282
+ buildAccessTokenAudience,
1283
+ buildAccessTokenPayload,
1284
+ buildIdTokenPayload,
1285
+ computeAtHash,
1286
+ generateIdToken,${randomStringImport}
1287
+ resolveAcrAmr,
1288
+ type TokenClientInfo,
1289
+ } from '${corePkg}';
1290
+ import { processCibaGrant } from '${EXPERIMENTAL_PACKAGE}/ciba';
1291
+ import type { RegisteredClient } from '../_oidc-provider/config';
1292
+ import {
1293
+ accessTokenIssuer,
1294
+ acrResolver,
1295
+ cibaAuthenticationRequestStore,
1296
+ config,
1297
+ selectIdTokenSigningKey,
1298
+ stores,
1299
+ type ProviderSigningKeys,
1300
+ } from '../_oidc-provider/provider';
1301
+ import { noStoreJson, oauthError } from '../_oidc-provider/http';
1302
+
1303
+ /**
1304
+ * Redeem an approved backchannel authentication request for tokens. Every
1305
+ * state but "approved" is thrown as CibaGrantError — authorization_pending /
1306
+ * slow_down / access_denied / expired_token, plus invalid_request /
1307
+ * invalid_grant (CIBA §11) — and answered by the token route.
1308
+ */
1309
+ export async function redeemCibaRequest(
1310
+ params: Record<string, string>,
1311
+ client: TokenClientInfo,
1312
+ keys: ProviderSigningKeys,
1313
+ ): Promise<Response> {
1314
+ const cibaGrant = await processCibaGrant({
1315
+ params,
1316
+ client,
1317
+ store: cibaAuthenticationRequestStore,
1318
+ });
1319
+
1320
+ ${approvedGrantTokens({
1321
+ grant: 'cibaGrant',
1322
+ refreshTokens: features.refreshToken,
1323
+ audienceNote: 'CIBA §7.1 has no resource parameter, so nothing else is\n // requested.',
1324
+ idTokenNote: `The backchannel authentication endpoint requires the openid scope, so an ID
1325
+ // Token is always issued. It carries no nonce (CIBA §7.1 defines no such
1326
+ // parameter, and OIDC Core 1.0 §2 only requires nonce when the authentication
1327
+ // request carried one) and no c_hash (there is no code). Poll mode adds no
1328
+ // CIBA-specific claims either — the auth_req_id claim of §10.3.1 belongs to
1329
+ // the push-mode token delivery message.`,
1330
+ grantIdNote: 'Inherit the grantId minted at approval so revoking the grant kills\n // every token issued from this backchannel authentication.',
1331
+ offlineNote: 'the approval screen the user\n // just went through',
1332
+ nonceNote: 'CIBA §7.1 defines no nonce parameter, so the re-issued ID Token has\n // none to preserve either.',
1333
+ })}
1334
+ }
1335
+ `;
1336
+ }
1337
+ //# sourceMappingURL=token.js.map