@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.
- 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 +284 -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,662 @@
|
|
|
1
|
+
import { DEFAULT_FEATURES } from '../../features.js';
|
|
2
|
+
import { EXPERIMENTAL_PACKAGE } from '../hono/templates.js';
|
|
3
|
+
const DEVICE_PACKAGE = `${EXPERIMENTAL_PACKAGE}/device-authorization-grant`;
|
|
4
|
+
export function nextJsDeviceAuthorizationConfigTemplate() {
|
|
5
|
+
return `/**
|
|
6
|
+
* EXPERIMENTAL — Device Authorization Grant settings (RFC 8628).
|
|
7
|
+
*
|
|
8
|
+
* Read by the device authorization endpoint and the verification UI (/device).
|
|
9
|
+
*
|
|
10
|
+
* - deviceCodeExpiresIn: §3.2 expires_in, in seconds. Keep it short: it is the
|
|
11
|
+
* window in which a user_code can be guessed (§5.1) or phished (§5.4).
|
|
12
|
+
* - pollInterval: §3.2 interval, in seconds. The token endpoint raises a
|
|
13
|
+
* record's own interval by 5 every time it answers slow_down.
|
|
14
|
+
* - maxLoginAttempts: failed device logins allowed per record before it is
|
|
15
|
+
* denied. Per-record only — see device/login/route.ts.
|
|
16
|
+
*
|
|
17
|
+
* Not configurable: the user_code charset (RFC 8628 §6.1 base-20) and length
|
|
18
|
+
* (8). They carry the entropy claim, so they are constants in the experimental
|
|
19
|
+
* package rather than something a config typo can weaken.
|
|
20
|
+
*/
|
|
21
|
+
export const deviceAuthorizationConfig = {
|
|
22
|
+
deviceCodeExpiresIn: 600,
|
|
23
|
+
pollInterval: 5,
|
|
24
|
+
maxLoginAttempts: 5,
|
|
25
|
+
};
|
|
26
|
+
`;
|
|
27
|
+
}
|
|
28
|
+
export function nextJsDeviceAuthorizationRouteTemplate(corePkg, features = DEFAULT_FEATURES, scopes = []) {
|
|
29
|
+
const customScopesDeclared = scopes.length > 0;
|
|
30
|
+
const customScopeImport = customScopesDeclared
|
|
31
|
+
? `
|
|
32
|
+
import { findUnsupportedScopes } from '../_oidc-provider/scopes';`
|
|
33
|
+
: '';
|
|
34
|
+
const customScopeStep = customScopesDeclared
|
|
35
|
+
? `
|
|
36
|
+
// RFC 6749 §3.3: reject a scope this OP never declared, the same way
|
|
37
|
+
// /authorize does. Checked after applyOfflineAccessPolicy so an
|
|
38
|
+
// offline_access the policy already dropped stays ignored rather than
|
|
39
|
+
// becoming invalid_scope (OIDC Core 1.0 §11).
|
|
40
|
+
const unsupportedScopes = findUnsupportedScopes(scope);
|
|
41
|
+
if (unsupportedScopes.length > 0) {
|
|
42
|
+
throw new DeviceAuthorizationError(
|
|
43
|
+
'invalid_scope',
|
|
44
|
+
'Unsupported scope: ' + unsupportedScopes.join(' '),
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
: '';
|
|
49
|
+
return `/**
|
|
50
|
+
* EXPERIMENTAL — Device Authorization Endpoint (RFC 8628 §3.1 / §3.2).
|
|
51
|
+
*
|
|
52
|
+
* Generated because the OP was created with \`--enable device-authorization-grant\`.
|
|
53
|
+
* Backed by ${EXPERIMENTAL_PACKAGE}, whose API is NOT stable: it may change in a
|
|
54
|
+
* breaking way between releases. Do not build production code on it without
|
|
55
|
+
* pinning the version.
|
|
56
|
+
*
|
|
57
|
+
* The device (a TV app, a CLI, an IoT box) POSTs here — back channel,
|
|
58
|
+
* client-authenticated — and receives a device_code it polls the token endpoint
|
|
59
|
+
* with, plus a short user_code the End-User types into /device on another
|
|
60
|
+
* device's browser.
|
|
61
|
+
*
|
|
62
|
+
* NOTE (RFC 8628 §5.1): rate limiting the user_code guess surface is left to
|
|
63
|
+
* the deployment layer (reverse proxy / platform). An in-process counter cannot
|
|
64
|
+
* work across instances, so putting one here would give a false sense of
|
|
65
|
+
* protection. The in-band defenses are the 20^8 user_code entropy, the short
|
|
66
|
+
* TTL, and answering every failed match identically.
|
|
67
|
+
*/
|
|
68
|
+
import {
|
|
69
|
+
DeviceAuthorizationError,
|
|
70
|
+
applyOfflineAccessPolicy,
|
|
71
|
+
buildDeviceAuthorizationResponse,
|
|
72
|
+
createDeviceAuthorizationRecord,
|
|
73
|
+
validateDeviceAuthorizationScope,
|
|
74
|
+
validateDeviceGrantAllowed,
|
|
75
|
+
} from '${DEVICE_PACKAGE}';
|
|
76
|
+
import {
|
|
77
|
+
TokenError,
|
|
78
|
+
extractClientCredentials,
|
|
79
|
+
resolveAuthenticatedTokenClient,
|
|
80
|
+
sanitizeErrorDescription,
|
|
81
|
+
validateClientAuthMethod,
|
|
82
|
+
verifyClientSecret,
|
|
83
|
+
} from '${corePkg}';
|
|
84
|
+
import { clientResolver, config, deviceAuthorizationStore } from '../_oidc-provider/provider';
|
|
85
|
+
import {
|
|
86
|
+
clientCors,
|
|
87
|
+
corsPreflight,
|
|
88
|
+
isFormUrlEncoded,
|
|
89
|
+
noStoreJson,
|
|
90
|
+
oauthError,
|
|
91
|
+
uniqueParams,
|
|
92
|
+
withCors,
|
|
93
|
+
} from '../_oidc-provider/http';${customScopeImport}
|
|
94
|
+
import { deviceAuthorizationConfig } from './config';
|
|
95
|
+
|
|
96
|
+
export const dynamic = 'force-dynamic';
|
|
97
|
+
export const runtime = 'nodejs';
|
|
98
|
+
|
|
99
|
+
export async function POST(request: Request): Promise<Response> {
|
|
100
|
+
return withCors(request, clientCors, await deviceAuthorization(request));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function OPTIONS(request: Request): Response {
|
|
104
|
+
return corsPreflight(request, clientCors);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* device_code is a credential, so every answer follows the token response rules
|
|
109
|
+
* of RFC 6749 §5.1 / §5.2 (no-store), although RFC 8628 §3.2 has no explicit rule.
|
|
110
|
+
*/
|
|
111
|
+
async function deviceAuthorization(request: Request): Promise<Response> {
|
|
112
|
+
// RFC 8628 §3.1: the body MUST be application/x-www-form-urlencoded.
|
|
113
|
+
if (!isFormUrlEncoded(request)) {
|
|
114
|
+
return oauthError('invalid_request', 'Device authorization requests must use application/x-www-form-urlencoded');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// RFC 6749 §3.1: request parameters MUST NOT be repeated.
|
|
118
|
+
const { params, duplicateKey } = uniqueParams(new URLSearchParams(await request.text()));
|
|
119
|
+
if (duplicateKey !== undefined) {
|
|
120
|
+
return oauthError('invalid_request', \`Parameter "\${sanitizeErrorDescription(duplicateKey)}" must not be repeated\`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
// --- Client authentication pipeline -------------------------------------
|
|
125
|
+
// RFC 8628 §3.1: "The client authentication requirements of Section 3.2.1 of
|
|
126
|
+
// [RFC6749] apply" — the same pipeline the token endpoint runs. Public
|
|
127
|
+
// clients present only client_id.
|
|
128
|
+
const presentedCredentials = extractClientCredentials({
|
|
129
|
+
params,
|
|
130
|
+
authorizationHeader: request.headers.get('Authorization') ?? '',
|
|
131
|
+
});
|
|
132
|
+
const client = await resolveAuthenticatedTokenClient(
|
|
133
|
+
presentedCredentials.clientId,
|
|
134
|
+
clientResolver,
|
|
135
|
+
);
|
|
136
|
+
validateClientAuthMethod(client, presentedCredentials);
|
|
137
|
+
await verifyClientSecret(client, presentedCredentials.clientSecret);
|
|
138
|
+
|
|
139
|
+
// --- Device authorization pipeline --------------------------------------
|
|
140
|
+
// Each step below is an independent function from
|
|
141
|
+
// ${DEVICE_PACKAGE}, called in RFC 8628 §3.1 order. Delete a call to drop
|
|
142
|
+
// that validation, or insert your own logic between steps.
|
|
143
|
+
|
|
144
|
+
// RFC 6749 §5.2: the client must be registered for the device_code grant.
|
|
145
|
+
validateDeviceGrantAllowed(client);
|
|
146
|
+
|
|
147
|
+
// RFC 8628 §3.1 leaves scope OPTIONAL, but this OP requires scope and openid
|
|
148
|
+
// everywhere (the same rule as /authorize), so a request without scope is
|
|
149
|
+
// rejected: a known, deliberate profile restriction.
|
|
150
|
+
const requestedScope = validateDeviceAuthorizationScope(params['scope']);
|
|
151
|
+
|
|
152
|
+
// OIDC Core 1.0 §11: drop offline_access when it could never be granted.
|
|
153
|
+
const scope = applyOfflineAccessPolicy(requestedScope, {
|
|
154
|
+
client,
|
|
155
|
+
refreshTokenFeatureEnabled: ${features.refreshToken ? 'true' : 'false'},
|
|
156
|
+
});
|
|
157
|
+
${customScopeStep}
|
|
158
|
+
// RFC 8628 §3.2 / §5.2: mint a 256-bit device_code and a collision-checked
|
|
159
|
+
// base-20 user_code, then store the pending record under both.
|
|
160
|
+
const record = await createDeviceAuthorizationRecord({
|
|
161
|
+
clientId: client.clientId,
|
|
162
|
+
scope,
|
|
163
|
+
store: deviceAuthorizationStore,
|
|
164
|
+
expiresIn: deviceAuthorizationConfig.deviceCodeExpiresIn,
|
|
165
|
+
interval: deviceAuthorizationConfig.pollInterval,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Never log device_code or user_code: both are live credentials for the
|
|
169
|
+
// lifetime of the record (RFC 8628 §5.1 / §5.2).
|
|
170
|
+
|
|
171
|
+
return noStoreJson(buildDeviceAuthorizationResponse(record, config.issuer));
|
|
172
|
+
} catch (error) {
|
|
173
|
+
if (error instanceof DeviceAuthorizationError) {
|
|
174
|
+
// RFC 6749 §5.2 shape. Authentication failures are core TokenErrors (401).
|
|
175
|
+
return oauthError(error.code, error.errorDescription, error.statusCode);
|
|
176
|
+
}
|
|
177
|
+
if (error instanceof TokenError) {
|
|
178
|
+
return oauthError(
|
|
179
|
+
error.error,
|
|
180
|
+
error.errorDescription,
|
|
181
|
+
error.statusCode,
|
|
182
|
+
error.wwwAuthenticate ? { 'WWW-Authenticate': error.wwwAuthenticate } : undefined,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return oauthError('server_error', undefined, 500);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
`;
|
|
189
|
+
}
|
|
190
|
+
export function nextJsDeviceScreensTemplate() {
|
|
191
|
+
return `/**
|
|
192
|
+
* EXPERIMENTAL — the screens of the device verification UI (RFC 8628 §3.3).
|
|
193
|
+
*
|
|
194
|
+
* Restyle the UI here. Keep the form actions, field names and decision values:
|
|
195
|
+
* the Route Handlers next to this file read exactly those.
|
|
196
|
+
*/
|
|
197
|
+
import {
|
|
198
|
+
DeviceAuthorizationError,
|
|
199
|
+
DeviceVerificationError,
|
|
200
|
+
INVALID_USER_CODE_MESSAGE,
|
|
201
|
+
} from '${DEVICE_PACKAGE}';
|
|
202
|
+
import { errorPage, escapeHtml, htmlResponse } from '../_oidc-provider/html';
|
|
203
|
+
|
|
204
|
+
/** The user_code entry form. userCode is untrusted input (query or a previous submission). */
|
|
205
|
+
export function verificationScreen(userCode: string, error?: string): Response {
|
|
206
|
+
const errorHtml = error ? \`<p style="color: red;">\${escapeHtml(error)}</p>\` : '';
|
|
207
|
+
return htmlResponse(
|
|
208
|
+
\`<!DOCTYPE html>
|
|
209
|
+
<html>
|
|
210
|
+
<head><title>Device Activation</title></head>
|
|
211
|
+
<body>
|
|
212
|
+
<h1>Device Activation</h1>
|
|
213
|
+
<p>Enter the code shown on your device.</p>
|
|
214
|
+
\${errorHtml}
|
|
215
|
+
<form method="POST" action="/device">
|
|
216
|
+
<div>
|
|
217
|
+
<label for="user_code">Code:</label>
|
|
218
|
+
<input type="text" id="user_code" name="user_code" value="\${escapeHtml(userCode)}" required />
|
|
219
|
+
</div>
|
|
220
|
+
<button type="submit">Continue</button>
|
|
221
|
+
</form>
|
|
222
|
+
</body>
|
|
223
|
+
</html>\`,
|
|
224
|
+
{ status: error ? 400 : 200 },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The code entry form again, with the one reason-free failure message. RFC 8628
|
|
230
|
+
* §5.1: unknown, expired and already-used codes must be indistinguishable, or
|
|
231
|
+
* the response itself confirms which codes exist.
|
|
232
|
+
*/
|
|
233
|
+
export function invalidUserCodeScreen(userCode: string): Response {
|
|
234
|
+
return verificationScreen(userCode, INVALID_USER_CODE_MESSAGE);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The sign-in form of the device flow; cookies carry the browser binding. */
|
|
238
|
+
export function loginScreen(
|
|
239
|
+
params: { userCode: string; csrfToken: string; error?: string; remainingAttempts?: number },
|
|
240
|
+
cookies: readonly string[] = [],
|
|
241
|
+
): Response {
|
|
242
|
+
const errorHtml = params.error
|
|
243
|
+
? \`<p style="color: red;">\${escapeHtml(params.error)}\${
|
|
244
|
+
params.remainingAttempts !== undefined
|
|
245
|
+
? \`. Attempts remaining: \${params.remainingAttempts}\`
|
|
246
|
+
: ''
|
|
247
|
+
}</p>\`
|
|
248
|
+
: '';
|
|
249
|
+
return htmlResponse(
|
|
250
|
+
\`<!DOCTYPE html>
|
|
251
|
+
<html>
|
|
252
|
+
<head><title>Login</title></head>
|
|
253
|
+
<body>
|
|
254
|
+
<h1>Login</h1>
|
|
255
|
+
<p>Activating device code <strong>\${escapeHtml(params.userCode)}</strong></p>
|
|
256
|
+
\${errorHtml}
|
|
257
|
+
<form method="POST" action="/device/login">
|
|
258
|
+
<input type="hidden" name="user_code" value="\${escapeHtml(params.userCode)}" />
|
|
259
|
+
<input type="hidden" name="csrf_token" value="\${escapeHtml(params.csrfToken)}" />
|
|
260
|
+
<div>
|
|
261
|
+
<label for="username">Username:</label>
|
|
262
|
+
<input type="text" id="username" name="username" required />
|
|
263
|
+
</div>
|
|
264
|
+
<div>
|
|
265
|
+
<label for="password">Password:</label>
|
|
266
|
+
<input type="password" id="password" name="password" required />
|
|
267
|
+
</div>
|
|
268
|
+
<button type="submit">Login</button>
|
|
269
|
+
</form>
|
|
270
|
+
</body>
|
|
271
|
+
</html>\`,
|
|
272
|
+
{ cookies },
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The approve / deny screen. RFC 8628 §5.4: the user_code is repeated so the
|
|
278
|
+
* End-User can check it against the device in front of them — the only defense
|
|
279
|
+
* against being lured into approving someone else's device.
|
|
280
|
+
*/
|
|
281
|
+
export function approvalScreen(
|
|
282
|
+
params: { userCode: string; csrfToken: string; clientId: string; scopes: readonly string[] },
|
|
283
|
+
cookies: readonly string[] = [],
|
|
284
|
+
): Response {
|
|
285
|
+
const scopeListHtml = params.scopes.map((scope) => \` <li>\${escapeHtml(scope)}</li>\`).join('\\n');
|
|
286
|
+
return htmlResponse(
|
|
287
|
+
\`<!DOCTYPE html>
|
|
288
|
+
<html>
|
|
289
|
+
<head><title>Authorize Device</title></head>
|
|
290
|
+
<body>
|
|
291
|
+
<h1>Authorize Device</h1>
|
|
292
|
+
<p>Confirm that your device is showing this code: <strong>\${escapeHtml(params.userCode)}</strong></p>
|
|
293
|
+
<p>Do not continue if the code does not match.</p>
|
|
294
|
+
<p>Client <strong>\${escapeHtml(params.clientId)}</strong> is requesting access to the following scopes:</p>
|
|
295
|
+
<ul>
|
|
296
|
+
\${scopeListHtml}
|
|
297
|
+
</ul>
|
|
298
|
+
<form method="POST" action="/device/approve">
|
|
299
|
+
<input type="hidden" name="user_code" value="\${escapeHtml(params.userCode)}" />
|
|
300
|
+
<input type="hidden" name="csrf_token" value="\${escapeHtml(params.csrfToken)}" />
|
|
301
|
+
<button type="submit" name="decision" value="approve">Approve</button>
|
|
302
|
+
<button type="submit" name="decision" value="deny">Deny</button>
|
|
303
|
+
</form>
|
|
304
|
+
</body>
|
|
305
|
+
</html>\`,
|
|
306
|
+
{ cookies },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** The "go back to your device" screen. */
|
|
311
|
+
export function completedScreen(
|
|
312
|
+
params: { approved: boolean; clientId: string },
|
|
313
|
+
cookies: readonly string[] = [],
|
|
314
|
+
): Response {
|
|
315
|
+
const outcome = params.approved
|
|
316
|
+
? \`<p>You approved <strong>\${escapeHtml(params.clientId)}</strong>.</p>\`
|
|
317
|
+
: \`<p>You denied <strong>\${escapeHtml(params.clientId)}</strong>.</p>\`;
|
|
318
|
+
return htmlResponse(
|
|
319
|
+
\`<!DOCTYPE html>
|
|
320
|
+
<html>
|
|
321
|
+
<head><title>Device Activation</title></head>
|
|
322
|
+
<body>
|
|
323
|
+
<h1>Device Activation</h1>
|
|
324
|
+
\${outcome}
|
|
325
|
+
<p>You can close this page and go back to your device.</p>
|
|
326
|
+
</body>
|
|
327
|
+
</html>\`,
|
|
328
|
+
{ cookies },
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* A failed binding or CSRF check, shown on the OP's error page with its status
|
|
334
|
+
* (403 for a missing binding). Anything that is not a verification failure is
|
|
335
|
+
* rethrown.
|
|
336
|
+
*/
|
|
337
|
+
export function verificationFailureScreen(error: unknown): Response {
|
|
338
|
+
if (error instanceof DeviceVerificationError) {
|
|
339
|
+
return errorPage(error.message, error.statusCode);
|
|
340
|
+
}
|
|
341
|
+
if (error instanceof DeviceAuthorizationError) {
|
|
342
|
+
return errorPage(error.errorDescription, 400);
|
|
343
|
+
}
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
`;
|
|
347
|
+
}
|
|
348
|
+
export function nextJsDeviceRouteTemplate(scopes = []) {
|
|
349
|
+
const customScopesDeclared = scopes.length > 0;
|
|
350
|
+
const customScopeImport = customScopesDeclared
|
|
351
|
+
? `
|
|
352
|
+
import { resolveGrantableScopes } from '../_oidc-provider/scopes';`
|
|
353
|
+
: '';
|
|
354
|
+
const approvalScopes = customScopesDeclared
|
|
355
|
+
? 'await resolveGrantableScopes(record.scope, session.subject)'
|
|
356
|
+
: 'record.scope';
|
|
357
|
+
return `/**
|
|
358
|
+
* EXPERIMENTAL — device verification UI, step 1 (RFC 8628 §3.3).
|
|
359
|
+
*
|
|
360
|
+
* The End-User opens /device on a second device and types the user_code the
|
|
361
|
+
* first device shows. On a match this step binds the verification to this
|
|
362
|
+
* browser: the user_code is known to whoever started the flow — possibly an
|
|
363
|
+
* attacker — so a CSRF token on the record alone would be no defense. The
|
|
364
|
+
* binding cookie minted here is what every later step requires (see
|
|
365
|
+
* buildDeviceBindingCookie() in store.ts for the full model).
|
|
366
|
+
*
|
|
367
|
+
* Backed by ${EXPERIMENTAL_PACKAGE}, whose API is NOT stable.
|
|
368
|
+
*/
|
|
369
|
+
import type { NextRequest } from 'next/server';
|
|
370
|
+
import {
|
|
371
|
+
findPendingRecordByUserCode,
|
|
372
|
+
issueVerificationBinding,
|
|
373
|
+
type DeviceAuthorizationRecord,
|
|
374
|
+
} from '${DEVICE_PACKAGE}';
|
|
375
|
+
import { deviceAuthorizationStore, stores } from '../_oidc-provider/provider';
|
|
376
|
+
import { buildDeviceBindingCookie, parseSessionId } from '../_oidc-provider/store';
|
|
377
|
+
import { readFormFields } from '../_oidc-provider/http';${customScopeImport}
|
|
378
|
+
import {
|
|
379
|
+
approvalScreen,
|
|
380
|
+
invalidUserCodeScreen,
|
|
381
|
+
loginScreen,
|
|
382
|
+
verificationScreen,
|
|
383
|
+
} from './screens';
|
|
384
|
+
|
|
385
|
+
export const dynamic = 'force-dynamic';
|
|
386
|
+
export const runtime = 'nodejs';
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* The user_code entry form (RFC 8628 §3.3 / §3.3.1). Unauthenticated and free
|
|
390
|
+
* of side effects: a user_code in the query (verification_uri_complete) only
|
|
391
|
+
* pre-fills the field, so following the complete URI consumes or reveals
|
|
392
|
+
* nothing.
|
|
393
|
+
*/
|
|
394
|
+
export function GET(request: NextRequest): Response {
|
|
395
|
+
return verificationScreen(request.nextUrl.searchParams.get('user_code') ?? '');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** User code submission: on a match, the sign-in form or — signed in already — the approval screen. */
|
|
399
|
+
export async function POST(request: Request): Promise<Response> {
|
|
400
|
+
const form = await readFormFields(request);
|
|
401
|
+
const submittedUserCode = String(form.get('user_code') ?? '');
|
|
402
|
+
|
|
403
|
+
const record = await findPendingRecordByUserCode(submittedUserCode, deviceAuthorizationStore);
|
|
404
|
+
if (!record) {
|
|
405
|
+
return invalidUserCodeScreen(submittedUserCode);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Rotate the binding secret and the csrf token together. A second browser
|
|
409
|
+
// submitting the same user_code takes the binding over (last writer wins);
|
|
410
|
+
// that is inherent to a flow whose identifier is shareable by design.
|
|
411
|
+
const { bindingSecret, csrfToken } = await issueVerificationBinding(
|
|
412
|
+
record,
|
|
413
|
+
deviceAuthorizationStore,
|
|
414
|
+
);
|
|
415
|
+
const bindingCookie = buildDeviceBindingCookie(
|
|
416
|
+
record.userCode,
|
|
417
|
+
bindingSecret,
|
|
418
|
+
remainingTtlSeconds(record),
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
const sessionId = parseSessionId(request.headers.get('Cookie'));
|
|
422
|
+
const session = sessionId ? await stores.browserSessionStore.get(sessionId) : undefined;
|
|
423
|
+
if (session) {
|
|
424
|
+
return approvalScreen(
|
|
425
|
+
{
|
|
426
|
+
userCode: record.userCodeDisplay,
|
|
427
|
+
csrfToken,
|
|
428
|
+
clientId: record.clientId,
|
|
429
|
+
scopes: ${approvalScopes},
|
|
430
|
+
},
|
|
431
|
+
[bindingCookie],
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return loginScreen({ userCode: record.userCodeDisplay, csrfToken }, [bindingCookie]);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Remaining lifetime of a record in whole seconds, rounded up so the binding
|
|
439
|
+
* cookie always outlives the record it binds (a cookie that expired first
|
|
440
|
+
* would turn a still-valid verification into an unexplained 403).
|
|
441
|
+
*/
|
|
442
|
+
function remainingTtlSeconds(record: DeviceAuthorizationRecord): number {
|
|
443
|
+
return Math.max(0, Math.ceil((record.expiresAt.getTime() - Date.now()) / 1000));
|
|
444
|
+
}
|
|
445
|
+
`;
|
|
446
|
+
}
|
|
447
|
+
export function nextJsDeviceLoginRouteTemplate(corePkg, scopes = []) {
|
|
448
|
+
const customScopesDeclared = scopes.length > 0;
|
|
449
|
+
const customScopeImport = customScopesDeclared
|
|
450
|
+
? `
|
|
451
|
+
import { resolveGrantableScopes } from '../../_oidc-provider/scopes';`
|
|
452
|
+
: '';
|
|
453
|
+
const approvalScopes = customScopesDeclared
|
|
454
|
+
? 'await resolveGrantableScopes(record.scope, user.sub)'
|
|
455
|
+
: 'record.scope';
|
|
456
|
+
return `/**
|
|
457
|
+
* EXPERIMENTAL — device verification UI, step 2: sign in (RFC 8628 §3.3).
|
|
458
|
+
*
|
|
459
|
+
* Binding first, then CSRF, then credentials: the binding proves this is the
|
|
460
|
+
* browser that submitted the user_code, and it must gate the step that would
|
|
461
|
+
* otherwise let a forged POST plant an OP session in the victim's browser
|
|
462
|
+
* (login CSRF).
|
|
463
|
+
*
|
|
464
|
+
* Backed by ${EXPERIMENTAL_PACKAGE}, whose API is NOT stable.
|
|
465
|
+
*/
|
|
466
|
+
import {
|
|
467
|
+
findPendingRecordByUserCode,
|
|
468
|
+
recordDeviceLoginFailure,
|
|
469
|
+
validateVerificationBinding,
|
|
470
|
+
validateVerificationCsrfToken,
|
|
471
|
+
} from '${DEVICE_PACKAGE}';
|
|
472
|
+
import { generateRandomString } from '${corePkg}';
|
|
473
|
+
import { errorPage } from '../../_oidc-provider/html';
|
|
474
|
+
import { readFormFields } from '../../_oidc-provider/http';
|
|
475
|
+
import { deviceAuthorizationStore, stores } from '../../_oidc-provider/provider';
|
|
476
|
+
import { buildSessionCookie, parseDeviceBindingSecret } from '../../_oidc-provider/store';${customScopeImport}
|
|
477
|
+
import { deviceAuthorizationConfig } from '../../device_authorization/config';
|
|
478
|
+
import {
|
|
479
|
+
approvalScreen,
|
|
480
|
+
invalidUserCodeScreen,
|
|
481
|
+
loginScreen,
|
|
482
|
+
verificationFailureScreen,
|
|
483
|
+
} from '../screens';
|
|
484
|
+
|
|
485
|
+
export const dynamic = 'force-dynamic';
|
|
486
|
+
export const runtime = 'nodejs';
|
|
487
|
+
|
|
488
|
+
export async function POST(request: Request): Promise<Response> {
|
|
489
|
+
const form = await readFormFields(request);
|
|
490
|
+
const submittedUserCode = String(form.get('user_code') ?? '');
|
|
491
|
+
const csrfToken = String(form.get('csrf_token') ?? '');
|
|
492
|
+
|
|
493
|
+
const record = await findPendingRecordByUserCode(submittedUserCode, deviceAuthorizationStore);
|
|
494
|
+
if (!record) {
|
|
495
|
+
return invalidUserCodeScreen(submittedUserCode);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
try {
|
|
499
|
+
await validateVerificationBinding(
|
|
500
|
+
record,
|
|
501
|
+
parseDeviceBindingSecret(request.headers.get('Cookie'), record.userCode),
|
|
502
|
+
);
|
|
503
|
+
validateVerificationCsrfToken(record, csrfToken);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
return verificationFailureScreen(error);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Swap point: replace this with your own credential check (LDAP, WebAuthn, an
|
|
509
|
+
// upstream IdP) without touching anything above or below it.
|
|
510
|
+
const user = await stores.userStore.authenticate(
|
|
511
|
+
String(form.get('username') ?? ''),
|
|
512
|
+
String(form.get('password') ?? ''),
|
|
513
|
+
);
|
|
514
|
+
if (!user) {
|
|
515
|
+
// Per-record throttling only. An attacker holding a device-grant client can
|
|
516
|
+
// mint unlimited records, so the aggregate password-guess budget is the same
|
|
517
|
+
// as the one on /login. Subject-scoped throttling is a separate concern.
|
|
518
|
+
const failure = await recordDeviceLoginFailure(
|
|
519
|
+
record,
|
|
520
|
+
deviceAuthorizationStore,
|
|
521
|
+
deviceAuthorizationConfig.maxLoginAttempts,
|
|
522
|
+
);
|
|
523
|
+
if (!failure.canRetry) {
|
|
524
|
+
// The record is now denied: the device gets access_denied on its next poll.
|
|
525
|
+
return errorPage('Too many login attempts', 429);
|
|
526
|
+
}
|
|
527
|
+
return loginScreen({
|
|
528
|
+
userCode: record.userCodeDisplay,
|
|
529
|
+
csrfToken,
|
|
530
|
+
error: 'Invalid credentials',
|
|
531
|
+
remainingAttempts: failure.remainingAttempts,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const sessionId = generateRandomString(32);
|
|
536
|
+
await stores.browserSessionStore.set(sessionId, {
|
|
537
|
+
subject: user.sub,
|
|
538
|
+
authTime: Math.floor(Date.now() / 1000),
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// The new OP session travels with the approval screen; the approval step has
|
|
542
|
+
// to present the binding cookie again as well.
|
|
543
|
+
return approvalScreen(
|
|
544
|
+
{
|
|
545
|
+
userCode: record.userCodeDisplay,
|
|
546
|
+
csrfToken,
|
|
547
|
+
clientId: record.clientId,
|
|
548
|
+
scopes: ${approvalScopes},
|
|
549
|
+
},
|
|
550
|
+
[buildSessionCookie(sessionId)],
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
`;
|
|
554
|
+
}
|
|
555
|
+
export function nextJsDeviceApproveRouteTemplate(scopes = []) {
|
|
556
|
+
const customScopesDeclared = scopes.length > 0;
|
|
557
|
+
const customScopeImport = customScopesDeclared
|
|
558
|
+
? `
|
|
559
|
+
import { resolveGrantableScopes } from '../../_oidc-provider/scopes';`
|
|
560
|
+
: '';
|
|
561
|
+
const approveNarrowStep = customScopesDeclared
|
|
562
|
+
? `
|
|
563
|
+
// Apply the scope policy to what was approved. approveDeviceAuthorization()
|
|
564
|
+
// copies the requested scope into approvedScope, so the policy is applied
|
|
565
|
+
// afterwards and persisted; the token endpoint reads approvedScope, and
|
|
566
|
+
// RFC 6749 §3.3 allows a granted scope narrower than the request. Write the
|
|
567
|
+
// policy in resolveGrantableScopes() (scopes.ts).
|
|
568
|
+
approved.approvedScope = await resolveGrantableScopes(
|
|
569
|
+
approved.approvedScope ?? approved.scope,
|
|
570
|
+
session.subject,
|
|
571
|
+
);
|
|
572
|
+
await deviceAuthorizationStore.update(approved);
|
|
573
|
+
`
|
|
574
|
+
: '';
|
|
575
|
+
return `/**
|
|
576
|
+
* EXPERIMENTAL — device verification UI, step 3: approve or deny
|
|
577
|
+
* (RFC 8628 §3.3).
|
|
578
|
+
*
|
|
579
|
+
* The only state-changing step of the UI, so it demands all three: an OP
|
|
580
|
+
* session, the binding cookie, and the csrf_token. The device learns the
|
|
581
|
+
* outcome by polling the token endpoint.
|
|
582
|
+
*
|
|
583
|
+
* Backed by ${EXPERIMENTAL_PACKAGE}, whose API is NOT stable.
|
|
584
|
+
*/
|
|
585
|
+
import {
|
|
586
|
+
approveDeviceAuthorization,
|
|
587
|
+
denyDeviceAuthorization,
|
|
588
|
+
findPendingRecordByUserCode,
|
|
589
|
+
validateVerificationBinding,
|
|
590
|
+
} from '${DEVICE_PACKAGE}';
|
|
591
|
+
import { errorPage } from '../../_oidc-provider/html';
|
|
592
|
+
import { readFormFields } from '../../_oidc-provider/http';
|
|
593
|
+
import { deviceAuthorizationStore, resolvers, stores } from '../../_oidc-provider/provider';
|
|
594
|
+
import {
|
|
595
|
+
buildClearedDeviceBindingCookie,
|
|
596
|
+
parseDeviceBindingSecret,
|
|
597
|
+
parseSessionId,
|
|
598
|
+
} from '../../_oidc-provider/store';${customScopeImport}
|
|
599
|
+
import { completedScreen, invalidUserCodeScreen, verificationFailureScreen } from '../screens';
|
|
600
|
+
|
|
601
|
+
export const dynamic = 'force-dynamic';
|
|
602
|
+
export const runtime = 'nodejs';
|
|
603
|
+
|
|
604
|
+
export async function POST(request: Request): Promise<Response> {
|
|
605
|
+
const form = await readFormFields(request);
|
|
606
|
+
const submittedUserCode = String(form.get('user_code') ?? '');
|
|
607
|
+
const csrfToken = String(form.get('csrf_token') ?? '');
|
|
608
|
+
|
|
609
|
+
const record = await findPendingRecordByUserCode(submittedUserCode, deviceAuthorizationStore);
|
|
610
|
+
if (!record) {
|
|
611
|
+
return invalidUserCodeScreen(submittedUserCode);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const sessionId = parseSessionId(request.headers.get('Cookie'));
|
|
615
|
+
const session = sessionId ? await stores.browserSessionStore.get(sessionId) : undefined;
|
|
616
|
+
if (!session) {
|
|
617
|
+
return errorPage('Sign in again to approve this device', 401);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// The decision ends the verification; drop its binding cookie.
|
|
621
|
+
const clearBindingCookie = buildClearedDeviceBindingCookie(record.userCode);
|
|
622
|
+
try {
|
|
623
|
+
await validateVerificationBinding(
|
|
624
|
+
record,
|
|
625
|
+
parseDeviceBindingSecret(request.headers.get('Cookie'), record.userCode),
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
if (String(form.get('decision') ?? '') === 'approve') {
|
|
629
|
+
// csrf_token is validated inside; the record moves to approved with the
|
|
630
|
+
// subject, auth_time, scope and a fresh grantId the token endpoint reads.
|
|
631
|
+
const approved = await approveDeviceAuthorization({
|
|
632
|
+
record,
|
|
633
|
+
store: deviceAuthorizationStore,
|
|
634
|
+
csrfToken,
|
|
635
|
+
subject: session.subject,
|
|
636
|
+
authTime: session.authTime,
|
|
637
|
+
});
|
|
638
|
+
${approveNarrowStep}
|
|
639
|
+
// Record the consent the way /consent does, so a later Authorization Code
|
|
640
|
+
// Flow for this client skips the consent screen (OIDC Core 1.0 §3.1.2.4),
|
|
641
|
+
// and the grant, so withdrawing that consent revokes these tokens too.
|
|
642
|
+
await resolvers.consentResolver.recordConsent?.(
|
|
643
|
+
session.subject,
|
|
644
|
+
approved.clientId,
|
|
645
|
+
approved.approvedScope ?? approved.scope,
|
|
646
|
+
);
|
|
647
|
+
if (approved.grantId) {
|
|
648
|
+
await resolvers.consentResolver.recordGrant(session.subject, approved.clientId, approved.grantId);
|
|
649
|
+
}
|
|
650
|
+
return completedScreen({ approved: true, clientId: approved.clientId }, [clearBindingCookie]);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Anything but 'approve' denies.
|
|
654
|
+
await denyDeviceAuthorization({ record, store: deviceAuthorizationStore, csrfToken });
|
|
655
|
+
return completedScreen({ approved: false, clientId: record.clientId }, [clearBindingCookie]);
|
|
656
|
+
} catch (error) {
|
|
657
|
+
return verificationFailureScreen(error);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
`;
|
|
661
|
+
}
|
|
662
|
+
//# sourceMappingURL=device.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device.js","sourceRoot":"","sources":["../../../src/frameworks/nextjs/device.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { OidcFeatureConfig } from '../../features.js';
|
|
2
|
+
export declare function nextJsUserinfoRouteTemplate(corePkg: string): string;
|
|
3
|
+
export declare function nextJsIntrospectionRouteTemplate(corePkg: string, features?: OidcFeatureConfig): string;
|
|
4
|
+
export declare function nextJsRevocationRouteTemplate(corePkg: string): string;
|
|
5
|
+
export declare function nextJsParConfigTemplate(): string;
|
|
6
|
+
export declare function nextJsParRouteTemplate(corePkg: string): string;
|
|
7
|
+
//# sourceMappingURL=endpoints.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../../../src/frameworks/nextjs/endpoints.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAI3D,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0MnE;AAGD,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE,iBAAoC,GAC7C,MAAM,CA+LR;AAGD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAwHrE;AAGD,wBAAgB,uBAAuB,IAAI,MAAM,CAsBhD;AAGD,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAkI9D"}
|