@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.
- package/README.md +83 -7
- package/dist/frameworks/hono/index.d.ts.map +1 -1
- package/dist/frameworks/hono/index.js +18 -9
- package/dist/frameworks/hono/index.js.map +1 -1
- package/dist/frameworks/hono/pages.d.ts +7 -6
- package/dist/frameworks/hono/pages.d.ts.map +1 -1
- package/dist/frameworks/hono/pages.js +62 -44
- package/dist/frameworks/hono/pages.js.map +1 -1
- package/dist/frameworks/hono/templates.d.ts +19 -6
- package/dist/frameworks/hono/templates.d.ts.map +1 -1
- package/dist/frameworks/hono/templates.js +439 -304
- package/dist/frameworks/hono/templates.js.map +1 -1
- package/dist/frameworks/hono/views.d.ts +3 -0
- package/dist/frameworks/hono/views.d.ts.map +1 -0
- package/dist/frameworks/hono/views.js +458 -0
- package/dist/frameworks/hono/views.js.map +1 -0
- package/dist/frameworks/nextjs/authorize.d.ts +3 -0
- package/dist/frameworks/nextjs/authorize.d.ts.map +1 -0
- package/dist/frameworks/nextjs/authorize.js +757 -0
- package/dist/frameworks/nextjs/authorize.js.map +1 -0
- package/dist/frameworks/nextjs/ciba.d.ts +8 -0
- package/dist/frameworks/nextjs/ciba.d.ts.map +1 -0
- package/dist/frameworks/nextjs/ciba.js +591 -0
- package/dist/frameworks/nextjs/ciba.js.map +1 -0
- package/dist/frameworks/nextjs/conformance.d.ts +3 -0
- package/dist/frameworks/nextjs/conformance.d.ts.map +1 -0
- package/dist/frameworks/nextjs/conformance.js +2447 -0
- package/dist/frameworks/nextjs/conformance.js.map +1 -0
- package/dist/frameworks/nextjs/device.d.ts +8 -0
- package/dist/frameworks/nextjs/device.d.ts.map +1 -0
- package/dist/frameworks/nextjs/device.js +662 -0
- package/dist/frameworks/nextjs/device.js.map +1 -0
- package/dist/frameworks/nextjs/endpoints.d.ts +7 -0
- package/dist/frameworks/nextjs/endpoints.d.ts.map +1 -0
- package/dist/frameworks/nextjs/endpoints.js +673 -0
- package/dist/frameworks/nextjs/endpoints.js.map +1 -0
- package/dist/frameworks/nextjs/index.d.ts.map +1 -1
- package/dist/frameworks/nextjs/index.js +135 -2
- package/dist/frameworks/nextjs/index.js.map +1 -1
- package/dist/frameworks/nextjs/interaction.d.ts +13 -0
- package/dist/frameworks/nextjs/interaction.d.ts.map +1 -0
- package/dist/frameworks/nextjs/interaction.js +864 -0
- package/dist/frameworks/nextjs/interaction.js.map +1 -0
- package/dist/frameworks/nextjs/logout.d.ts +5 -0
- package/dist/frameworks/nextjs/logout.d.ts.map +1 -0
- package/dist/frameworks/nextjs/logout.js +271 -0
- package/dist/frameworks/nextjs/logout.js.map +1 -0
- package/dist/frameworks/nextjs/metadata.d.ts +6 -0
- package/dist/frameworks/nextjs/metadata.d.ts.map +1 -0
- package/dist/frameworks/nextjs/metadata.js +339 -0
- package/dist/frameworks/nextjs/metadata.js.map +1 -0
- package/dist/frameworks/nextjs/provider.d.ts +6 -0
- package/dist/frameworks/nextjs/provider.d.ts.map +1 -0
- package/dist/frameworks/nextjs/provider.js +797 -0
- package/dist/frameworks/nextjs/provider.js.map +1 -0
- package/dist/frameworks/nextjs/token.d.ts +7 -0
- package/dist/frameworks/nextjs/token.d.ts.map +1 -0
- package/dist/frameworks/nextjs/token.js +1337 -0
- package/dist/frameworks/nextjs/token.js.map +1 -0
- package/dist/frameworks/web-standard/templates.d.ts +1 -13
- package/dist/frameworks/web-standard/templates.d.ts.map +1 -1
- package/dist/frameworks/web-standard/templates.js +12 -1296
- package/dist/frameworks/web-standard/templates.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -8
- package/dist/index.js.map +1 -1
- 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
|