@udibo/oauth2 0.26.0 → 0.27.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 +4 -2
- package/dist/adapters/hono/authorization-server.d.ts +21 -8
- package/dist/adapters/hono/authorization-server.js +2 -2
- package/dist/cli/idp/server.js +35 -14
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +58 -0
- package/dist/models/authentication.d.ts +28 -0
- package/dist/models/authorization-code.d.ts +3 -3
- package/dist/models/responses.d.ts +8 -2
- package/dist/server/authentication-request.d.ts +60 -0
- package/dist/server/authentication-request.js +102 -0
- package/dist/server/authorization-server.d.ts +55 -7
- package/dist/server/authorization-server.js +54 -11
- package/dist/server/mod.d.ts +3 -2
- package/dist/server/mod.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,8 +89,10 @@ The package implements these specifications, with the limits noted:
|
|
|
89
89
|
- [RFC 9068](https://www.rfc-editor.org/rfc/rfc9068) JWT access tokens, opt-in;
|
|
90
90
|
access tokens are opaque by default.
|
|
91
91
|
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html):
|
|
92
|
-
the authorization code flow, ES256 ID tokens, `nonce`, and
|
|
93
|
-
|
|
92
|
+
the authorization code flow, ES256 ID tokens, `nonce`, UserInfo, and the
|
|
93
|
+
`prompt`, `max_age` and `acr_values` controls, with `auth_time` and `acr` in
|
|
94
|
+
the ID token. Your login callback performs any re-authentication they need,
|
|
95
|
+
and there is no `claims` request parameter.
|
|
94
96
|
- OpenID Connect [Discovery 1.0](https://openid.net/specs/openid-connect-discovery-1_0.html)
|
|
95
97
|
and [RP-Initiated Logout 1.0](https://openid.net/specs/openid-connect-rpinitiated-1_0.html).
|
|
96
98
|
- [OpenID Connect Back-Channel Logout 1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html),
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
import { Hono } from "hono";
|
|
36
36
|
import type { Context, Handler, MiddlewareHandler } from "hono";
|
|
37
37
|
import type { RequireConditions } from "../../models/authorization.ts";
|
|
38
|
-
import type { AuthenticationContext } from "../../models/authentication.ts";
|
|
38
|
+
import type { AuthenticationContext, AuthenticationRequest } from "../../models/authentication.ts";
|
|
39
39
|
import type { ClientInterface } from "../../models/client.ts";
|
|
40
40
|
import type { AbstractScope, BasicScope } from "../../models/scope.ts";
|
|
41
41
|
import { AuthorizationServer, type AuthorizationServerOptions } from "../../server/authorization-server.ts";
|
|
@@ -47,20 +47,29 @@ import type { AuthenticatedContext } from "../../server/resource-server.ts";
|
|
|
47
47
|
* or session variables without reconstructing a {@link Request}. Return one
|
|
48
48
|
* of:
|
|
49
49
|
*
|
|
50
|
-
* - `{ user, authorizedScope?, authenticationContext? }` —
|
|
51
|
-
* the authorize flow continues. Supply only verified
|
|
52
|
-
* `authenticationContext`; the code and its tokens retain
|
|
50
|
+
* - `{ user, authorizedScope?, authenticationContext?, reauthenticated? }` —
|
|
51
|
+
* user is authenticated; the authorize flow continues. Supply only verified
|
|
52
|
+
* event claims in `authenticationContext`; the code and its tokens retain
|
|
53
|
+
* that snapshot. Set `reauthenticated: true` only when the user actively
|
|
54
|
+
* authenticated in response to this authorization request.
|
|
53
55
|
* - `null` — explicit denial; the framework redirects to `redirect_uri` with
|
|
54
56
|
* `error=access_denied`.
|
|
55
57
|
* - A `Response` (typically `c.redirect(...)` to a login page) —
|
|
56
58
|
* short-circuits the authorize flow and is returned to the browser as-is.
|
|
57
59
|
* This is how login redirects, interstitials, etc. are wired without
|
|
58
|
-
* wrapping the framework handler.
|
|
60
|
+
* wrapping the framework handler. Under `prompt=none` the server answers
|
|
61
|
+
* `login_required` instead.
|
|
62
|
+
*
|
|
63
|
+
* The second argument, always passed, carries the request's validated
|
|
64
|
+
* `prompt`, `max_age` and `acr_values`. The server answers `login_required`
|
|
65
|
+
* when the returned user fails `satisfiesAuthenticationRequest`, so send the
|
|
66
|
+
* user to sign in again whenever the session cannot meet it.
|
|
59
67
|
*/
|
|
60
|
-
export type HonoAuthenticateUserFn<User> = (c: Context) => Promise<{
|
|
68
|
+
export type HonoAuthenticateUserFn<User> = (c: Context, authentication?: AuthenticationRequest) => Promise<{
|
|
61
69
|
user: User;
|
|
62
70
|
authorizedScope?: AbstractScope;
|
|
63
71
|
authenticationContext?: AuthenticationContext;
|
|
72
|
+
reauthenticated?: boolean;
|
|
64
73
|
} | Response | null>;
|
|
65
74
|
/**
|
|
66
75
|
* Collects consent from the resource owner for the requested scope.
|
|
@@ -76,9 +85,13 @@ export type HonoAuthenticateUserFn<User> = (c: Context) => Promise<{
|
|
|
76
85
|
* - A `Response` (typically `c.html(...)` rendering a consent page) —
|
|
77
86
|
* short-circuits the authorize flow and is returned to the browser as-is.
|
|
78
87
|
* The consent UI should resubmit to the authorize endpoint with a
|
|
79
|
-
* decision parameter the callback can read on the next request.
|
|
88
|
+
* decision parameter the callback can read on the next request. Under
|
|
89
|
+
* `prompt=none` the server answers `consent_required` instead.
|
|
90
|
+
*
|
|
91
|
+
* With `prompt=consent` the server calls it even when the scope is already
|
|
92
|
+
* authorized, and the fifth argument, always passed, says so.
|
|
80
93
|
*/
|
|
81
|
-
export type HonoHandleConsentFn<Client extends ClientInterface, S extends AbstractScope> = (c: Context, client: Client, requestedScope: S | undefined, user: unknown) => Promise<{
|
|
94
|
+
export type HonoHandleConsentFn<Client extends ClientInterface, S extends AbstractScope> = (c: Context, client: Client, requestedScope: S | undefined, user: unknown, authentication?: AuthenticationRequest) => Promise<{
|
|
82
95
|
approved: boolean;
|
|
83
96
|
scope?: S;
|
|
84
97
|
} | Response>;
|
|
@@ -104,9 +104,9 @@ export class HonoAuthorizationServer extends AuthorizationServer {
|
|
|
104
104
|
authorizeHandler(options) {
|
|
105
105
|
const { authenticateUser, handleConsent } = options;
|
|
106
106
|
return (c) => {
|
|
107
|
-
const auth = () => authenticateUser(c);
|
|
107
|
+
const auth = (_request, authentication) => authenticateUser(c, authentication);
|
|
108
108
|
const consent = handleConsent
|
|
109
|
-
? (client, requestedScope, user) => handleConsent(c, client, requestedScope, user)
|
|
109
|
+
? (client, requestedScope, user, authentication) => handleConsent(c, client, requestedScope, user, authentication)
|
|
110
110
|
: undefined;
|
|
111
111
|
return this.handleAuthorizeRequest(c.req.raw, auth, consent);
|
|
112
112
|
};
|
package/dist/cli/idp/server.js
CHANGED
|
@@ -21,6 +21,7 @@ import { AuthorizationCodeGrant } from "../../server/grants/authorization-code.j
|
|
|
21
21
|
import { ClientCredentialsGrant } from "../../server/grants/client-credentials.js";
|
|
22
22
|
import { PasswordGrant } from "../../server/grants/password.js";
|
|
23
23
|
import { RefreshTokenGrant } from "../../server/grants/refresh-token.js";
|
|
24
|
+
import { satisfiesAuthenticationRequest } from "../../server/authentication-request.js";
|
|
24
25
|
import { StaticSigningKeyProvider, } from "../../server/signing-keys.js";
|
|
25
26
|
import { generateCodeChallenge, generateCodeVerifier, generateState, } from "../../utils/pkce.js";
|
|
26
27
|
import { DEFAULT_MAX_BODY_BYTES, readBoundedBody } from "../../utils/_body.js";
|
|
@@ -43,6 +44,14 @@ const WILDCARD_HOSTNAMES = new Set(["0.0.0.0", "::", "[::]"]);
|
|
|
43
44
|
export function isLoopbackHostname(hostname) {
|
|
44
45
|
return LOOPBACK_HOSTNAMES.has(hostname) || hostname.startsWith("127.");
|
|
45
46
|
}
|
|
47
|
+
const REAUTHENTICATION_WINDOW_SECONDS = 600;
|
|
48
|
+
function newSession(userId, signedInFor) {
|
|
49
|
+
return {
|
|
50
|
+
userId,
|
|
51
|
+
authTime: Math.floor(Date.now() / 1000),
|
|
52
|
+
...(signedInFor === undefined ? {} : { signedInFor }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
46
55
|
async function createState(config, signingKey, issuer) {
|
|
47
56
|
const userService = new MemoryUserService();
|
|
48
57
|
const clientService = new MemoryClientService(userService);
|
|
@@ -181,21 +190,33 @@ function formValue(form, name) {
|
|
|
181
190
|
const value = form.get(name);
|
|
182
191
|
return typeof value === "string" ? value : undefined;
|
|
183
192
|
}
|
|
184
|
-
|
|
193
|
+
function currentSession(state, request) {
|
|
185
194
|
const sessionId = readCookie(request, SESSION_COOKIE);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
return await state.userService.get(userId);
|
|
195
|
+
return sessionId ? state.sessions.get(sessionId) : undefined;
|
|
196
|
+
}
|
|
197
|
+
async function currentUser(state, request) {
|
|
198
|
+
const session = currentSession(state, request);
|
|
199
|
+
return session ? await state.userService.get(session.userId) : undefined;
|
|
192
200
|
}
|
|
193
201
|
function authenticateUserFor(state, config, request) {
|
|
194
|
-
return async () => {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
202
|
+
return async (_request, authentication) => {
|
|
203
|
+
const returnTo = pathAndQuery(request);
|
|
204
|
+
const session = currentSession(state, request);
|
|
205
|
+
const user = session && (await state.userService.get(session.userId));
|
|
206
|
+
if (session && user) {
|
|
207
|
+
const result = {
|
|
208
|
+
user,
|
|
209
|
+
authenticationContext: { auth_time: session.authTime },
|
|
210
|
+
reauthenticated: session.signedInFor === returnTo &&
|
|
211
|
+
Math.floor(Date.now() / 1000) - session.authTime <=
|
|
212
|
+
REAUTHENTICATION_WINDOW_SECONDS,
|
|
213
|
+
};
|
|
214
|
+
if (!authentication ||
|
|
215
|
+
satisfiesAuthenticationRequest(authentication, result)) {
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return htmlResponse(loginPage({ returnTo, users: config.users }));
|
|
199
220
|
};
|
|
200
221
|
}
|
|
201
222
|
function handleConsentFor(state, request) {
|
|
@@ -258,7 +279,7 @@ async function handleLoginSubmission(state, config, request) {
|
|
|
258
279
|
return htmlResponse(loginPage({ returnTo, users: config.users, error }), 401);
|
|
259
280
|
}
|
|
260
281
|
const sessionId = crypto.randomUUID();
|
|
261
|
-
state.sessions.set(sessionId, user.id);
|
|
282
|
+
state.sessions.set(sessionId, newSession(user.id, returnTo));
|
|
262
283
|
return redirect(returnTo, {
|
|
263
284
|
"set-cookie": cookie(SESSION_COOKIE, sessionId),
|
|
264
285
|
});
|
|
@@ -334,7 +355,7 @@ async function handleAdminSession(state, request) {
|
|
|
334
355
|
return jsonError(400, 'no seeded user matched "userId" or "username"');
|
|
335
356
|
}
|
|
336
357
|
const sessionId = crypto.randomUUID();
|
|
337
|
-
state.sessions.set(sessionId, user.id);
|
|
358
|
+
state.sessions.set(sessionId, newSession(user.id));
|
|
338
359
|
return new Response(JSON.stringify({
|
|
339
360
|
sessionId,
|
|
340
361
|
cookie: SESSION_COOKIE,
|
package/dist/errors.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - RFC 6749 Section 5.2 (Token errors)
|
|
7
7
|
* - RFC 6750 Section 3.1 (Bearer token errors)
|
|
8
8
|
* - RFC 8628 Section 3.5 (Device flow errors)
|
|
9
|
+
* - OpenID Connect Core 1.0 Section 3.1.2.6 (Authentication errors)
|
|
9
10
|
*
|
|
10
11
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
|
11
12
|
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
|
|
@@ -204,3 +205,36 @@ export declare const SlowDownError: OAuth2ErrorClass;
|
|
|
204
205
|
* @see https://datatracker.ietf.org/doc/html/rfc8628#section-3.5
|
|
205
206
|
*/
|
|
206
207
|
export declare const ExpiredTokenError: OAuth2ErrorClass;
|
|
208
|
+
/**
|
|
209
|
+
* The authorization request requires the user to authenticate, and the server
|
|
210
|
+
* cannot show a login page (`prompt=none`) or cannot re-authenticate the user
|
|
211
|
+
* as `prompt=login` or `max_age` requires.
|
|
212
|
+
*
|
|
213
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
214
|
+
*/
|
|
215
|
+
export declare const LoginRequiredError: OAuth2ErrorClass;
|
|
216
|
+
/**
|
|
217
|
+
* The authorization request requires the user's consent, and the server
|
|
218
|
+
* cannot obtain it: `prompt=none` forbids the consent page, or `prompt=consent`
|
|
219
|
+
* reached a server with no consent handler.
|
|
220
|
+
*
|
|
221
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
222
|
+
*/
|
|
223
|
+
export declare const ConsentRequiredError: OAuth2ErrorClass;
|
|
224
|
+
/**
|
|
225
|
+
* The authorization request requires user interaction other than login or
|
|
226
|
+
* consent, and `prompt=none` forbids showing it. Throw it from
|
|
227
|
+
* `authenticateUser` or `handleConsent` for an interstitial the request
|
|
228
|
+
* cannot skip.
|
|
229
|
+
*
|
|
230
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
231
|
+
*/
|
|
232
|
+
export declare const InteractionRequiredError: OAuth2ErrorClass;
|
|
233
|
+
/**
|
|
234
|
+
* The user must choose among several accounts and the server cannot ask:
|
|
235
|
+
* `prompt=none` with more than one signed-in account, or `prompt=select_account`
|
|
236
|
+
* that the application cannot honor.
|
|
237
|
+
*
|
|
238
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
239
|
+
*/
|
|
240
|
+
export declare const AccountSelectionRequiredError: OAuth2ErrorClass;
|
package/dist/errors.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - RFC 6749 Section 5.2 (Token errors)
|
|
7
7
|
* - RFC 6750 Section 3.1 (Bearer token errors)
|
|
8
8
|
* - RFC 8628 Section 3.5 (Device flow errors)
|
|
9
|
+
* - OpenID Connect Core 1.0 Section 3.1.2.6 (Authentication errors)
|
|
9
10
|
*
|
|
10
11
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
|
11
12
|
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
|
|
@@ -286,3 +287,60 @@ export const ExpiredTokenError = createHttpErrorClass({
|
|
|
286
287
|
error: "expired_token",
|
|
287
288
|
},
|
|
288
289
|
});
|
|
290
|
+
/**
|
|
291
|
+
* The authorization request requires the user to authenticate, and the server
|
|
292
|
+
* cannot show a login page (`prompt=none`) or cannot re-authenticate the user
|
|
293
|
+
* as `prompt=login` or `max_age` requires.
|
|
294
|
+
*
|
|
295
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
296
|
+
*/
|
|
297
|
+
export const LoginRequiredError = createHttpErrorClass({
|
|
298
|
+
name: "Login Required",
|
|
299
|
+
status: 400,
|
|
300
|
+
extensions: {
|
|
301
|
+
error: "login_required",
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
/**
|
|
305
|
+
* The authorization request requires the user's consent, and the server
|
|
306
|
+
* cannot obtain it: `prompt=none` forbids the consent page, or `prompt=consent`
|
|
307
|
+
* reached a server with no consent handler.
|
|
308
|
+
*
|
|
309
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
310
|
+
*/
|
|
311
|
+
export const ConsentRequiredError = createHttpErrorClass({
|
|
312
|
+
name: "Consent Required",
|
|
313
|
+
status: 400,
|
|
314
|
+
extensions: {
|
|
315
|
+
error: "consent_required",
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
/**
|
|
319
|
+
* The authorization request requires user interaction other than login or
|
|
320
|
+
* consent, and `prompt=none` forbids showing it. Throw it from
|
|
321
|
+
* `authenticateUser` or `handleConsent` for an interstitial the request
|
|
322
|
+
* cannot skip.
|
|
323
|
+
*
|
|
324
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
325
|
+
*/
|
|
326
|
+
export const InteractionRequiredError = createHttpErrorClass({
|
|
327
|
+
name: "Interaction Required",
|
|
328
|
+
status: 400,
|
|
329
|
+
extensions: {
|
|
330
|
+
error: "interaction_required",
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
/**
|
|
334
|
+
* The user must choose among several accounts and the server cannot ask:
|
|
335
|
+
* `prompt=none` with more than one signed-in account, or `prompt=select_account`
|
|
336
|
+
* that the application cannot honor.
|
|
337
|
+
*
|
|
338
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
339
|
+
*/
|
|
340
|
+
export const AccountSelectionRequiredError = createHttpErrorClass({
|
|
341
|
+
name: "Account Selection Required",
|
|
342
|
+
status: 400,
|
|
343
|
+
extensions: {
|
|
344
|
+
error: "account_selection_required",
|
|
345
|
+
},
|
|
346
|
+
});
|
|
@@ -22,3 +22,31 @@ export interface AuthenticationContext {
|
|
|
22
22
|
/** Truthful authentication methods; omitted when the complete set is unknown. */
|
|
23
23
|
readonly amr?: readonly string[];
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The OpenID Connect authentication controls of one authorization request,
|
|
27
|
+
* validated by the authorization server and handed to the application's
|
|
28
|
+
* `authenticateUser` and `handleConsent` callbacks.
|
|
29
|
+
*
|
|
30
|
+
* The server enforces what it can verify: `prompt=none` never reaches a login
|
|
31
|
+
* or consent page, and a code is issued only when the returned authentication
|
|
32
|
+
* satisfies `prompt=login` and `max_age` (see
|
|
33
|
+
* `satisfiesAuthenticationRequest`). `acrValues` is a preference the
|
|
34
|
+
* application meets as it can; report the `acr` it actually achieved.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const authentication: AuthenticationRequest = {
|
|
39
|
+
* prompt: ["login"],
|
|
40
|
+
* maxAge: 300,
|
|
41
|
+
* acrValues: ["urn:example:mfa"],
|
|
42
|
+
* };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export interface AuthenticationRequest {
|
|
46
|
+
/** Requested `prompt` values, each one the server supports; empty when absent. */
|
|
47
|
+
readonly prompt: readonly string[];
|
|
48
|
+
/** Requested `max_age` in seconds, when present. */
|
|
49
|
+
readonly maxAge?: number;
|
|
50
|
+
/** Requested `acr_values` in preference order; empty when absent. */
|
|
51
|
+
readonly acrValues: readonly string[];
|
|
52
|
+
}
|
|
@@ -38,11 +38,11 @@ export interface AuthorizationCode<Client extends ClientInterface, User, Scope e
|
|
|
38
38
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
|
|
39
39
|
*/
|
|
40
40
|
export interface AuthorizeParameters {
|
|
41
|
-
/** Raw space-delimited ACR preferences;
|
|
41
|
+
/** Raw space-delimited ACR preferences; `authenticateUser` receives them validated. */
|
|
42
42
|
acrValues?: string;
|
|
43
|
-
/** Raw maximum authentication age in seconds;
|
|
43
|
+
/** Raw maximum authentication age in seconds; the authorize endpoint validates and enforces it. */
|
|
44
44
|
maxAge?: string;
|
|
45
|
-
/** Raw prompt values;
|
|
45
|
+
/** Raw prompt values; the authorize endpoint validates them and enforces `none`, `login` and `consent`. */
|
|
46
46
|
prompt?: string;
|
|
47
47
|
/** Must be "code" for authorization code flow. */
|
|
48
48
|
responseType?: string;
|
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* Includes the RFC 6750 Section 3.1 bearer-token error codes
|
|
5
5
|
* (`invalid_token`, `insufficient_scope`) emitted by resource servers, in
|
|
6
6
|
* addition to the RFC 6749 Section 5.2 / Section 4.1.2.1 codes and the RFC
|
|
7
|
-
* 8628 device-flow codes.
|
|
7
|
+
* 8628 device-flow codes and the OpenID Connect Core §3.1.2.6
|
|
8
|
+
* authentication-error codes.
|
|
8
9
|
*
|
|
9
10
|
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-5.2
|
|
10
11
|
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
|
|
12
|
+
* @see https://openid.net/specs/openid-connect-core-1_0.html#AuthError
|
|
11
13
|
*/
|
|
12
|
-
export type OAuth2ErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "unauthorized_client" | "unsupported_grant_type" | "unsupported_response_type" | "unsupported_token_type" | "invalid_scope" | "invalid_token" | "insufficient_scope" | "access_denied" | "server_error" | "temporarily_unavailable" | "authorization_pending" | "slow_down" | "expired_token";
|
|
14
|
+
export type OAuth2ErrorCode = "invalid_request" | "invalid_client" | "invalid_grant" | "unauthorized_client" | "unsupported_grant_type" | "unsupported_response_type" | "unsupported_token_type" | "invalid_scope" | "invalid_token" | "insufficient_scope" | "access_denied" | "server_error" | "temporarily_unavailable" | "authorization_pending" | "slow_down" | "expired_token" | "login_required" | "consent_required" | "interaction_required" | "account_selection_required";
|
|
13
15
|
/**
|
|
14
16
|
* Standard OAuth2 error response.
|
|
15
17
|
*
|
|
@@ -74,6 +76,10 @@ export interface AuthorizationServerMetadata {
|
|
|
74
76
|
id_token_signing_alg_values_supported?: string[];
|
|
75
77
|
/** Subject identifier types supported (OIDC Discovery). */
|
|
76
78
|
subject_types_supported?: string[];
|
|
79
|
+
/** `prompt` values the authorization endpoint honors (OIDC Prompt Create). */
|
|
80
|
+
prompt_values_supported?: string[];
|
|
81
|
+
/** Authentication context class references the server can achieve (OIDC Discovery). */
|
|
82
|
+
acr_values_supported?: string[];
|
|
77
83
|
}
|
|
78
84
|
/**
|
|
79
85
|
* Device authorization response as defined in RFC 8628.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { AuthenticationContext, AuthenticationRequest } from "../models/authentication.ts";
|
|
2
|
+
/** The `prompt` values the authorization server enforces on its own. */
|
|
3
|
+
export declare const DEFAULT_PROMPT_VALUES_SUPPORTED: readonly string[];
|
|
4
|
+
/**
|
|
5
|
+
* What an `authenticateUser` callback reports about the authentication behind
|
|
6
|
+
* the user it returns, as {@linkcode satisfiesAuthenticationRequest} reads it.
|
|
7
|
+
*/
|
|
8
|
+
export interface AuthenticationOutcome {
|
|
9
|
+
/** The verified authentication event, when known. */
|
|
10
|
+
authenticationContext?: AuthenticationContext;
|
|
11
|
+
/**
|
|
12
|
+
* `true` only when the user actively authenticated in response to this
|
|
13
|
+
* authorization request, which is what `prompt=login` and `max_age=0` ask
|
|
14
|
+
* for. A session that merely exists, or was refreshed, is not that.
|
|
15
|
+
*/
|
|
16
|
+
reauthenticated?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export declare function assertSupportedValues(name: string, values: readonly string[]): readonly string[];
|
|
19
|
+
/**
|
|
20
|
+
* Validates the `prompt`, `max_age` and `acr_values` of an authorization
|
|
21
|
+
* request. Parameters sent without a value are treated as omitted (OIDC Core
|
|
22
|
+
* §3.1.2.1).
|
|
23
|
+
*
|
|
24
|
+
* @throws {InvalidRequestError} For a repeated parameter, a `prompt` value not
|
|
25
|
+
* in `promptValuesSupported`, `none` combined with another `prompt` value, or
|
|
26
|
+
* a `max_age` that is not a non-negative integer.
|
|
27
|
+
*/
|
|
28
|
+
export declare function parseAuthenticationRequest(params: URLSearchParams, promptValuesSupported: readonly string[]): AuthenticationRequest;
|
|
29
|
+
/**
|
|
30
|
+
* Why `outcome` does not satisfy `request`, as an `error_description`, or
|
|
31
|
+
* `undefined` when it does.
|
|
32
|
+
*/
|
|
33
|
+
export declare function authenticationShortfall(request: AuthenticationRequest, outcome: AuthenticationOutcome, now: number): string | undefined;
|
|
34
|
+
/**
|
|
35
|
+
* Whether an authentication meets the request's `prompt=login` and `max_age`
|
|
36
|
+
* controls — the check the authorization server applies to what
|
|
37
|
+
* `authenticateUser` returns before it issues a code. Call it from
|
|
38
|
+
* `authenticateUser` with the session's event to decide whether to send the
|
|
39
|
+
* user to sign in again instead of returning them.
|
|
40
|
+
*
|
|
41
|
+
* `prompt=login` and `max_age=0`, which OIDC Core makes equivalent, need
|
|
42
|
+
* `reauthenticated: true`. Any `max_age` needs a recorded `auth_time`, since
|
|
43
|
+
* the ID token must carry one, never more than a minute in the future. Above
|
|
44
|
+
* zero, it must also be no older than `max_age` seconds, whether or not the
|
|
45
|
+
* user re-authenticated. `acr_values` is a preference and never fails the
|
|
46
|
+
* check.
|
|
47
|
+
*
|
|
48
|
+
* @param now The current time in epoch milliseconds. Defaults to `Date.now()`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { satisfiesAuthenticationRequest } from "@udibo/oauth2/server";
|
|
53
|
+
*
|
|
54
|
+
* const fresh = satisfiesAuthenticationRequest(
|
|
55
|
+
* { prompt: [], maxAge: 300, acrValues: [] },
|
|
56
|
+
* { authenticationContext: { auth_time: Math.floor(Date.now() / 1000) } },
|
|
57
|
+
* );
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function satisfiesAuthenticationRequest(request: AuthenticationRequest, outcome: AuthenticationOutcome, now?: number): boolean;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { InvalidRequestError } from "../errors.js";
|
|
2
|
+
/** The `prompt` values the authorization server enforces on its own. */
|
|
3
|
+
export const DEFAULT_PROMPT_VALUES_SUPPORTED = Object.freeze(["none", "login", "consent"]);
|
|
4
|
+
const MAX_AUTH_TIME_SKEW_SECONDS = 60;
|
|
5
|
+
function spaceDelimited(value) {
|
|
6
|
+
return [...new Set((value ?? "").split(" ").filter(Boolean))];
|
|
7
|
+
}
|
|
8
|
+
export function assertSupportedValues(name, values) {
|
|
9
|
+
if (values.some((value) => typeof value !== "string" || !/^[\x21-\x7e]+$/.test(value))) {
|
|
10
|
+
throw new TypeError(`${name} entries must be non-empty strings of visible ASCII without spaces`);
|
|
11
|
+
}
|
|
12
|
+
return Object.freeze([...new Set(values)]);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validates the `prompt`, `max_age` and `acr_values` of an authorization
|
|
16
|
+
* request. Parameters sent without a value are treated as omitted (OIDC Core
|
|
17
|
+
* §3.1.2.1).
|
|
18
|
+
*
|
|
19
|
+
* @throws {InvalidRequestError} For a repeated parameter, a `prompt` value not
|
|
20
|
+
* in `promptValuesSupported`, `none` combined with another `prompt` value, or
|
|
21
|
+
* a `max_age` that is not a non-negative integer.
|
|
22
|
+
*/
|
|
23
|
+
export function parseAuthenticationRequest(params, promptValuesSupported) {
|
|
24
|
+
for (const name of ["prompt", "max_age", "acr_values"]) {
|
|
25
|
+
if (params.getAll(name).length > 1) {
|
|
26
|
+
throw new InvalidRequestError(`${name} parameter must not be repeated`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const prompt = spaceDelimited(params.get("prompt"));
|
|
30
|
+
if (prompt.some((value) => !promptValuesSupported.includes(value))) {
|
|
31
|
+
throw new InvalidRequestError("prompt value not supported");
|
|
32
|
+
}
|
|
33
|
+
if (prompt.includes("none") && prompt.length > 1) {
|
|
34
|
+
throw new InvalidRequestError("prompt=none cannot be combined with other prompt values");
|
|
35
|
+
}
|
|
36
|
+
const rawMaxAge = params.get("max_age") || undefined;
|
|
37
|
+
if (rawMaxAge !== undefined &&
|
|
38
|
+
(!/^\d+$/.test(rawMaxAge) || !Number.isSafeInteger(Number(rawMaxAge)))) {
|
|
39
|
+
throw new InvalidRequestError("max_age must be a non-negative integer");
|
|
40
|
+
}
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
prompt: Object.freeze(prompt),
|
|
43
|
+
...(rawMaxAge === undefined ? {} : { maxAge: Number(rawMaxAge) }),
|
|
44
|
+
acrValues: Object.freeze(spaceDelimited(params.get("acr_values"))),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Why `outcome` does not satisfy `request`, as an `error_description`, or
|
|
49
|
+
* `undefined` when it does.
|
|
50
|
+
*/
|
|
51
|
+
export function authenticationShortfall(request, outcome, now) {
|
|
52
|
+
const reauthenticated = outcome.reauthenticated === true;
|
|
53
|
+
if (request.prompt.includes("login") && !reauthenticated) {
|
|
54
|
+
return "prompt=login requires the user to authenticate again";
|
|
55
|
+
}
|
|
56
|
+
if (request.maxAge === 0 && !reauthenticated) {
|
|
57
|
+
return "max_age=0 requires the user to authenticate again";
|
|
58
|
+
}
|
|
59
|
+
if (request.maxAge === undefined)
|
|
60
|
+
return undefined;
|
|
61
|
+
const authTime = outcome.authenticationContext?.auth_time;
|
|
62
|
+
if (typeof authTime !== "number" || !Number.isFinite(authTime)) {
|
|
63
|
+
return "max_age requires the time the user authenticated";
|
|
64
|
+
}
|
|
65
|
+
const age = Math.floor(now / 1000) - authTime;
|
|
66
|
+
if (age < -MAX_AUTH_TIME_SKEW_SECONDS) {
|
|
67
|
+
return "the user's authentication time is in the future";
|
|
68
|
+
}
|
|
69
|
+
if (request.maxAge > 0 && age > request.maxAge) {
|
|
70
|
+
return "the user's last authentication is older than max_age";
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether an authentication meets the request's `prompt=login` and `max_age`
|
|
76
|
+
* controls — the check the authorization server applies to what
|
|
77
|
+
* `authenticateUser` returns before it issues a code. Call it from
|
|
78
|
+
* `authenticateUser` with the session's event to decide whether to send the
|
|
79
|
+
* user to sign in again instead of returning them.
|
|
80
|
+
*
|
|
81
|
+
* `prompt=login` and `max_age=0`, which OIDC Core makes equivalent, need
|
|
82
|
+
* `reauthenticated: true`. Any `max_age` needs a recorded `auth_time`, since
|
|
83
|
+
* the ID token must carry one, never more than a minute in the future. Above
|
|
84
|
+
* zero, it must also be no older than `max_age` seconds, whether or not the
|
|
85
|
+
* user re-authenticated. `acr_values` is a preference and never fails the
|
|
86
|
+
* check.
|
|
87
|
+
*
|
|
88
|
+
* @param now The current time in epoch milliseconds. Defaults to `Date.now()`.
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { satisfiesAuthenticationRequest } from "@udibo/oauth2/server";
|
|
93
|
+
*
|
|
94
|
+
* const fresh = satisfiesAuthenticationRequest(
|
|
95
|
+
* { prompt: [], maxAge: 300, acrValues: [] },
|
|
96
|
+
* { authenticationContext: { auth_time: Math.floor(Date.now() / 1000) } },
|
|
97
|
+
* );
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function satisfiesAuthenticationRequest(request, outcome, now = Date.now()) {
|
|
101
|
+
return authenticationShortfall(request, outcome, now) === undefined;
|
|
102
|
+
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* @module
|
|
22
22
|
*/
|
|
23
23
|
import type { AuthorizeParameters } from "../models/authorization-code.ts";
|
|
24
|
-
import type { AuthenticationContext } from "../models/authentication.ts";
|
|
24
|
+
import type { AuthenticationContext, AuthenticationRequest } from "../models/authentication.ts";
|
|
25
25
|
import type { ClientInterface } from "../models/client.ts";
|
|
26
26
|
import type { Token, TokenResponse } from "../models/token.ts";
|
|
27
27
|
import type { AuthorizationServerMetadata } from "../models/responses.ts";
|
|
@@ -288,6 +288,32 @@ export interface AuthorizationServerOptions<Client extends ClientInterface, User
|
|
|
288
288
|
* integer.
|
|
289
289
|
*/
|
|
290
290
|
maxBodyBytes?: number;
|
|
291
|
+
/**
|
|
292
|
+
* The `prompt` values the authorization endpoint accepts, advertised as
|
|
293
|
+
* `prompt_values_supported`. A request carrying any other value is refused
|
|
294
|
+
* with `invalid_request`.
|
|
295
|
+
*
|
|
296
|
+
* The server enforces `none`, `login` and `consent` itself. List another
|
|
297
|
+
* value, such as `select_account` or `create`, only when your
|
|
298
|
+
* `authenticateUser` honors it: the server passes it through in the
|
|
299
|
+
* {@linkcode AuthenticationRequest} and cannot check the result.
|
|
300
|
+
*
|
|
301
|
+
* @default ["none", "login", "consent"]
|
|
302
|
+
* @throws {TypeError} From the constructor when an entry is empty or
|
|
303
|
+
* contains a space or non-ASCII character.
|
|
304
|
+
*/
|
|
305
|
+
promptValuesSupported?: readonly string[];
|
|
306
|
+
/**
|
|
307
|
+
* Authentication context class references your `authenticateUser` can
|
|
308
|
+
* achieve, advertised as `acr_values_supported` in the OIDC discovery
|
|
309
|
+
* document. It does not restrict `acr_values`: the request's preferences
|
|
310
|
+
* reach `authenticateUser` either way, and the ID token's `acr` is what the
|
|
311
|
+
* returned `authenticationContext` records.
|
|
312
|
+
*
|
|
313
|
+
* @throws {TypeError} From the constructor when an entry is empty or
|
|
314
|
+
* contains a space or non-ASCII character.
|
|
315
|
+
*/
|
|
316
|
+
acrValuesSupported?: readonly string[];
|
|
291
317
|
}
|
|
292
318
|
/**
|
|
293
319
|
* Function to authenticate the user during authorization.
|
|
@@ -305,17 +331,29 @@ export interface AuthorizationServerOptions<Client extends ClientInterface, User
|
|
|
305
331
|
* pre-authorize everything, return the requested scope as `authorizedScope`.
|
|
306
332
|
* Supply only verified event claims in `authenticationContext`; the code,
|
|
307
333
|
* token and refresh descendants retain a snapshot. Omit unknown claims.
|
|
334
|
+
* Set `reauthenticated: true` only when the user actively authenticated in
|
|
335
|
+
* response to this authorization request.
|
|
308
336
|
* - `null` — explicit denial. The framework redirects the browser to
|
|
309
337
|
* `redirect_uri` with `error=access_denied`. Use this for "the user said
|
|
310
338
|
* no", not for "the user isn't signed in yet."
|
|
311
339
|
* - A `Response` — short-circuits the rest of the authorize flow and is
|
|
312
340
|
* returned to the browser as-is. Use this to redirect to a login page,
|
|
313
|
-
* render an interstitial, etc.
|
|
341
|
+
* render an interstitial, etc. Under `prompt=none` the server answers
|
|
342
|
+
* `login_required` instead of returning it.
|
|
343
|
+
*
|
|
344
|
+
* The second argument carries the request's validated `prompt`, `max_age` and
|
|
345
|
+
* `acr_values`; the server always passes it. Before issuing a code the server
|
|
346
|
+
* checks the returned user against it with
|
|
347
|
+
* {@linkcode satisfiesAuthenticationRequest} and answers `login_required` when
|
|
348
|
+
* the check fails, so send the user to sign in again (return a `Response`)
|
|
349
|
+
* whenever the session cannot meet it. Report the `acr` the login achieved in
|
|
350
|
+
* `authenticationContext`, never the requested one.
|
|
314
351
|
*/
|
|
315
|
-
export type AuthenticateUserFn<User> = (request: Request) => Promise<{
|
|
352
|
+
export type AuthenticateUserFn<User> = (request: Request, authentication?: AuthenticationRequest) => Promise<{
|
|
316
353
|
user: User;
|
|
317
354
|
authorizedScope?: AbstractScope;
|
|
318
355
|
authenticationContext?: AuthenticationContext;
|
|
356
|
+
reauthenticated?: boolean;
|
|
319
357
|
} | Response | null>;
|
|
320
358
|
/**
|
|
321
359
|
* Function to handle user consent during authorization.
|
|
@@ -330,9 +368,14 @@ export type AuthenticateUserFn<User> = (request: Request) => Promise<{
|
|
|
330
368
|
* `redirect_uri` with `error=access_denied`.
|
|
331
369
|
* - A `Response` — short-circuits the rest of the authorize flow and is
|
|
332
370
|
* returned to the browser as-is. Use this to render a consent page that
|
|
333
|
-
* later resubmits to the authorize endpoint with the user's decision.
|
|
371
|
+
* later resubmits to the authorize endpoint with the user's decision. Under
|
|
372
|
+
* `prompt=none` the server answers `consent_required` instead.
|
|
373
|
+
*
|
|
374
|
+
* With `prompt=consent` the server calls it even when the scope is already
|
|
375
|
+
* authorized, and the fourth argument, always passed, says so: ask again
|
|
376
|
+
* rather than reusing a stored grant.
|
|
334
377
|
*/
|
|
335
|
-
export type HandleConsentFn<Client extends ClientInterface, S extends AbstractScope> = (client: Client, requestedScope: S | undefined, user: unknown) => Promise<{
|
|
378
|
+
export type HandleConsentFn<Client extends ClientInterface, S extends AbstractScope> = (client: Client, requestedScope: S | undefined, user: unknown, authentication?: AuthenticationRequest) => Promise<{
|
|
336
379
|
approved: boolean;
|
|
337
380
|
scope?: S;
|
|
338
381
|
} | Response>;
|
|
@@ -450,7 +493,8 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
|
|
|
450
493
|
* @throws {RangeError} If `maxBodyBytes` or `maxTokenBodyBytes` is not a
|
|
451
494
|
* positive integer.
|
|
452
495
|
* @throws {TypeError} If `realm` contains a character outside printable
|
|
453
|
-
* ASCII
|
|
496
|
+
* ASCII, or a `promptValuesSupported` or `acrValuesSupported` entry is
|
|
497
|
+
* empty or contains a space or non-ASCII character.
|
|
454
498
|
*/
|
|
455
499
|
constructor(options: AuthorizationServerOptions<Client, User, S>);
|
|
456
500
|
/**
|
|
@@ -508,7 +552,9 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
|
|
|
508
552
|
* `openid`, or `undefined` when the OIDC surface is off (no signing keys),
|
|
509
553
|
* the token has no user, the scope lacks `openid`, or no issuer resolved.
|
|
510
554
|
* The id_token's `aud` is the client id and it expires one hour after
|
|
511
|
-
* issue.
|
|
555
|
+
* issue. Its `auth_time` and `acr` are the token's recorded
|
|
556
|
+
* `authenticationContext`, present exactly when the context has them,
|
|
557
|
+
* whatever `userClaims` returns.
|
|
512
558
|
*/
|
|
513
559
|
mintIdToken(token: Token<Client, User, S>, context: AuthorizationServerContext<Client, User, S>): Promise<string | undefined>;
|
|
514
560
|
/**
|
|
@@ -828,6 +874,8 @@ export { AbstractAuthorizationCodeService, type AbstractAuthorizationCodeService
|
|
|
828
874
|
export type { DeviceAuthorizationServiceInterface } from "./services/device-authorization.ts";
|
|
829
875
|
export type { UserServiceInterface } from "./services/user.ts";
|
|
830
876
|
export type { AuthorizationCode, AuthorizeParameters, } from "../models/authorization-code.ts";
|
|
877
|
+
export type { AuthenticationRequest } from "../models/authentication.ts";
|
|
878
|
+
export { type AuthenticationOutcome, satisfiesAuthenticationRequest, } from "./authentication-request.ts";
|
|
831
879
|
export type { DeviceAuthorization } from "../models/device-authorization.ts";
|
|
832
880
|
export type { AuthorizationServerMetadata, DeviceAuthorizationResponse, ErrorResponse, OAuth2ErrorCode, } from "../models/responses.ts";
|
|
833
881
|
export { challengeMethods, CODE_VERIFIER_MAX_LENGTH, CODE_VERIFIER_MIN_LENGTH, CODE_VERIFIER_PATTERN, generateCodeChallenge, generateCodeVerifier, generateState, getChallengeMethod, validateCodeChallenge, validateCodeVerifier, verifyCodeChallenge, } from "../utils/pkce.ts";
|
|
@@ -21,8 +21,9 @@
|
|
|
21
21
|
* @module
|
|
22
22
|
*/
|
|
23
23
|
import { snapshotAuthenticationContext } from "../utils/authentication-context.js";
|
|
24
|
-
import { AccessDeniedError, InvalidRequestError, InvalidTokenError, isOAuth2Error, OAuth2Error, ServerError, toOAuth2Error, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, } from "../errors.js";
|
|
24
|
+
import { AccessDeniedError, ConsentRequiredError, InvalidRequestError, InvalidTokenError, isOAuth2Error, LoginRequiredError, OAuth2Error, ServerError, toOAuth2Error, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, } from "../errors.js";
|
|
25
25
|
import { authenticateClientCredentials, extractClientCredentials, } from "./client-authentication.js";
|
|
26
|
+
import { assertSupportedValues, authenticationShortfall, DEFAULT_PROMPT_VALUES_SUPPORTED, parseAuthenticationRequest, } from "./authentication-request.js";
|
|
26
27
|
import { isRedirectUriPattern, matchRedirectUri } from "./redirect-uri.js";
|
|
27
28
|
import { AuthorizationCodeGrant } from "./grants/authorization-code.js";
|
|
28
29
|
import { DeviceAuthorizationGrant } from "./grants/device-authorization.js";
|
|
@@ -194,6 +195,8 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
194
195
|
#endSession;
|
|
195
196
|
#isPublicSuffix;
|
|
196
197
|
#maxBodyBytes;
|
|
198
|
+
#promptValuesSupported;
|
|
199
|
+
#acrValuesSupported;
|
|
197
200
|
#subjectOf;
|
|
198
201
|
#userClaims;
|
|
199
202
|
#introspectionClaims;
|
|
@@ -208,7 +211,8 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
208
211
|
* @throws {RangeError} If `maxBodyBytes` or `maxTokenBodyBytes` is not a
|
|
209
212
|
* positive integer.
|
|
210
213
|
* @throws {TypeError} If `realm` contains a character outside printable
|
|
211
|
-
* ASCII
|
|
214
|
+
* ASCII, or a `promptValuesSupported` or `acrValuesSupported` entry is
|
|
215
|
+
* empty or contains a space or non-ASCII character.
|
|
212
216
|
*/
|
|
213
217
|
constructor(options) {
|
|
214
218
|
super({
|
|
@@ -242,6 +246,10 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
242
246
|
this.#isPublicSuffix = options.isPublicSuffix;
|
|
243
247
|
this.#maxBodyBytes = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
244
248
|
assertMaxBodyBytes(this.#maxBodyBytes, "maxBodyBytes");
|
|
249
|
+
this.#promptValuesSupported = assertSupportedValues("promptValuesSupported", options.promptValuesSupported ?? DEFAULT_PROMPT_VALUES_SUPPORTED);
|
|
250
|
+
if (options.acrValuesSupported) {
|
|
251
|
+
this.#acrValuesSupported = assertSupportedValues("acrValuesSupported", options.acrValuesSupported);
|
|
252
|
+
}
|
|
245
253
|
this.#subjectOf = options.subjectOf ?? defaultSubjectOf;
|
|
246
254
|
this.#userClaims = options.userClaims;
|
|
247
255
|
this.#introspectionClaims = options.introspectionClaims;
|
|
@@ -466,7 +474,9 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
466
474
|
* `openid`, or `undefined` when the OIDC surface is off (no signing keys),
|
|
467
475
|
* the token has no user, the scope lacks `openid`, or no issuer resolved.
|
|
468
476
|
* The id_token's `aud` is the client id and it expires one hour after
|
|
469
|
-
* issue.
|
|
477
|
+
* issue. Its `auth_time` and `acr` are the token's recorded
|
|
478
|
+
* `authenticationContext`, present exactly when the context has them,
|
|
479
|
+
* whatever `userClaims` returns.
|
|
470
480
|
*/
|
|
471
481
|
async mintIdToken(token, context) {
|
|
472
482
|
if (!this.signingKeys || !token.user || !context.issuer)
|
|
@@ -486,6 +496,15 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
486
496
|
claims.nonce = token.nonce;
|
|
487
497
|
else
|
|
488
498
|
delete claims.nonce;
|
|
499
|
+
const event = token.authenticationContext;
|
|
500
|
+
if (event?.auth_time !== undefined)
|
|
501
|
+
claims.auth_time = event.auth_time;
|
|
502
|
+
else
|
|
503
|
+
delete claims.auth_time;
|
|
504
|
+
if (event?.acr !== undefined)
|
|
505
|
+
claims.acr = event.acr;
|
|
506
|
+
else
|
|
507
|
+
delete claims.acr;
|
|
489
508
|
return await signJwt(key, claims);
|
|
490
509
|
}
|
|
491
510
|
async #oidcClaims(user, scope, authenticationContext) {
|
|
@@ -785,12 +804,21 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
785
804
|
this.#validateResponseType(params.responseType);
|
|
786
805
|
const requestedScope = grant.parseScope(params.scope);
|
|
787
806
|
this.#validateChallenge(grant, params);
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
807
|
+
const authentication = parseAuthenticationRequest(new URL(request.url).searchParams, this.#promptValuesSupported);
|
|
808
|
+
const silent = authentication.prompt.includes("none");
|
|
809
|
+
const authResult = await authenticateUser(request, authentication);
|
|
810
|
+
if (authResult instanceof Response) {
|
|
811
|
+
if (!silent)
|
|
812
|
+
return authResult;
|
|
813
|
+
await authResult.body?.cancel();
|
|
814
|
+
throw new LoginRequiredError("the user must sign in");
|
|
815
|
+
}
|
|
791
816
|
if (!authResult) {
|
|
792
817
|
throw new AccessDeniedError("authentication required");
|
|
793
818
|
}
|
|
819
|
+
const shortfall = authenticationShortfall(authentication, authResult, Date.now());
|
|
820
|
+
if (shortfall)
|
|
821
|
+
throw new LoginRequiredError(shortfall);
|
|
794
822
|
const { user, authorizedScope } = authResult;
|
|
795
823
|
const authenticationContext = snapshotAuthenticationContext(authResult.authenticationContext);
|
|
796
824
|
if (grant.requirePKCE && !params.challenge) {
|
|
@@ -804,6 +832,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
804
832
|
authorizedScope,
|
|
805
833
|
services,
|
|
806
834
|
handleConsent,
|
|
835
|
+
authentication,
|
|
807
836
|
});
|
|
808
837
|
if (consented instanceof Response)
|
|
809
838
|
return consented;
|
|
@@ -909,14 +938,23 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
909
938
|
}
|
|
910
939
|
}
|
|
911
940
|
async #resolveScopeAndConsent(options) {
|
|
912
|
-
const { grant, client, user, requestedScope, authorizedScope, services, handleConsent, } = options;
|
|
941
|
+
const { grant, client, user, requestedScope, authorizedScope, services, handleConsent, authentication, } = options;
|
|
913
942
|
const acceptedScope = await grant.acceptedScope(client, user, requestedScope, services.tokenService);
|
|
914
943
|
let scope = acceptedScope ?? undefined;
|
|
915
|
-
const
|
|
944
|
+
const forceConsent = authentication.prompt.includes("consent");
|
|
945
|
+
if (forceConsent && !handleConsent) {
|
|
946
|
+
throw new ConsentRequiredError("prompt=consent requires a consent handler");
|
|
947
|
+
}
|
|
948
|
+
const needsConsent = forceConsent ||
|
|
949
|
+
(scope !== undefined && !(authorizedScope && authorizedScope.has(scope)));
|
|
916
950
|
if (needsConsent && handleConsent) {
|
|
917
|
-
const consent = await handleConsent(client, scope, user);
|
|
918
|
-
if (consent instanceof Response)
|
|
919
|
-
|
|
951
|
+
const consent = await handleConsent(client, scope, user, authentication);
|
|
952
|
+
if (consent instanceof Response) {
|
|
953
|
+
if (!authentication.prompt.includes("none"))
|
|
954
|
+
return consent;
|
|
955
|
+
await consent.body?.cancel();
|
|
956
|
+
throw new ConsentRequiredError("the user must consent");
|
|
957
|
+
}
|
|
920
958
|
if (!consent.approved) {
|
|
921
959
|
throw new AccessDeniedError("user denied consent");
|
|
922
960
|
}
|
|
@@ -1197,6 +1235,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1197
1235
|
metadata.response_types_supported = hasAuthCodeGrant ? ["code"] : [];
|
|
1198
1236
|
if (hasAuthCodeGrant) {
|
|
1199
1237
|
metadata.authorization_response_iss_parameter_supported = true;
|
|
1238
|
+
metadata.prompt_values_supported = [...this.#promptValuesSupported];
|
|
1200
1239
|
}
|
|
1201
1240
|
const scopesSupported = context.scopesSupported ?? this.scopesSupported;
|
|
1202
1241
|
if (scopesSupported) {
|
|
@@ -1206,6 +1245,9 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1206
1245
|
this.#advertiseEndpoints(metadata, endpoints, true);
|
|
1207
1246
|
metadata.id_token_signing_alg_values_supported = ["ES256"];
|
|
1208
1247
|
metadata.subject_types_supported = ["public"];
|
|
1248
|
+
if (hasAuthCodeGrant && this.#acrValuesSupported?.length) {
|
|
1249
|
+
metadata.acr_values_supported = [...this.#acrValuesSupported];
|
|
1250
|
+
}
|
|
1209
1251
|
}
|
|
1210
1252
|
return metadata;
|
|
1211
1253
|
}
|
|
@@ -1404,6 +1446,7 @@ export { PasswordGrant } from "./grants/password.js";
|
|
|
1404
1446
|
export { DeviceAuthorizationGrant } from "./grants/device-authorization.js";
|
|
1405
1447
|
export { AbstractTokenService, } from "./services/token.js";
|
|
1406
1448
|
export { AbstractAuthorizationCodeService, } from "./services/authorization-code.js";
|
|
1449
|
+
export { satisfiesAuthenticationRequest, } from "./authentication-request.js";
|
|
1407
1450
|
export { challengeMethods, CODE_VERIFIER_MAX_LENGTH, CODE_VERIFIER_MIN_LENGTH, CODE_VERIFIER_PATTERN, generateCodeChallenge, generateCodeVerifier, generateState, getChallengeMethod, validateCodeChallenge, validateCodeVerifier, verifyCodeChallenge, } from "../utils/pkce.js";
|
|
1408
1451
|
export { encodeBasicAuth, parseBasicAuth, tryParseBasicAuth, } from "../utils/basic-auth.js";
|
|
1409
1452
|
export { checkRedirectUriPattern, isRedirectUriPattern, matchRedirectUri, } from "./redirect-uri.js";
|
package/dist/server/mod.d.ts
CHANGED
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
11
|
export type { ClientCredentials, ClientInterface } from "../models/client.ts";
|
|
12
|
-
export type { AuthenticationContext } from "../models/authentication.ts";
|
|
12
|
+
export type { AuthenticationContext, AuthenticationRequest, } from "../models/authentication.ts";
|
|
13
13
|
export type { IntrospectionResponse, RefreshToken, Token, TokenResponse, } from "../models/token.ts";
|
|
14
14
|
export { AbstractScope, BasicScope } from "../models/scope.ts";
|
|
15
15
|
export type { ScopeConstructor } from "../models/scope.ts";
|
|
16
16
|
export { Authorization, authorizationFromClaims, } from "../models/authorization.ts";
|
|
17
17
|
export type { AuthorizationFailure, AuthorizationInit, OrganizationContext, RequireConditions, } from "../models/authorization.ts";
|
|
18
|
-
export { AccessDeniedError, AuthorizationPendingError, ExpiredTokenError, InsufficientPermissionsError, InsufficientScopeError, InvalidClientError, InvalidGrantError, InvalidRequestError, InvalidScopeError, InvalidTokenError, isOAuth2Error, OAuth2Error, ServerError, SlowDownError, TemporarilyUnavailableError, toOAuth2Error, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError, } from "../errors.ts";
|
|
18
|
+
export { AccessDeniedError, AccountSelectionRequiredError, AuthorizationPendingError, ConsentRequiredError, ExpiredTokenError, InsufficientPermissionsError, InsufficientScopeError, InteractionRequiredError, InvalidClientError, InvalidGrantError, InvalidRequestError, InvalidScopeError, InvalidTokenError, isOAuth2Error, LoginRequiredError, OAuth2Error, ServerError, SlowDownError, TemporarilyUnavailableError, toOAuth2Error, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError, } from "../errors.ts";
|
|
19
19
|
export type { OAuth2ErrorClass, OAuth2ProblemDetailsExtensions, } from "../errors.ts";
|
|
20
|
+
export { type AuthenticationOutcome, satisfiesAuthenticationRequest, } from "./authentication-request.ts";
|
|
20
21
|
export type { TokenReaderInterface } from "./services/token.ts";
|
|
21
22
|
export { checkRedirectUriPattern, isRedirectUriPattern, matchRedirectUri, } from "./redirect-uri.ts";
|
|
22
23
|
export type { IsPublicSuffix, RedirectUriPatternRule, RedirectUriPatternViolation, } from "./redirect-uri.ts";
|
package/dist/server/mod.js
CHANGED
|
@@ -10,5 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
export { AbstractScope, BasicScope } from "../models/scope.js";
|
|
12
12
|
export { Authorization, authorizationFromClaims, } from "../models/authorization.js";
|
|
13
|
-
export { AccessDeniedError, AuthorizationPendingError, ExpiredTokenError, InsufficientPermissionsError, InsufficientScopeError, InvalidClientError, InvalidGrantError, InvalidRequestError, InvalidScopeError, InvalidTokenError, isOAuth2Error, OAuth2Error, ServerError, SlowDownError, TemporarilyUnavailableError, toOAuth2Error, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError, } from "../errors.js";
|
|
13
|
+
export { AccessDeniedError, AccountSelectionRequiredError, AuthorizationPendingError, ConsentRequiredError, ExpiredTokenError, InsufficientPermissionsError, InsufficientScopeError, InteractionRequiredError, InvalidClientError, InvalidGrantError, InvalidRequestError, InvalidScopeError, InvalidTokenError, isOAuth2Error, LoginRequiredError, OAuth2Error, ServerError, SlowDownError, TemporarilyUnavailableError, toOAuth2Error, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, UnsupportedTokenTypeError, } from "../errors.js";
|
|
14
|
+
export { satisfiesAuthenticationRequest, } from "./authentication-request.js";
|
|
14
15
|
export { checkRedirectUriPattern, isRedirectUriPattern, matchRedirectUri, } from "./redirect-uri.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udibo/oauth2",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "A flexible OAuth2 toolkit: framework-agnostic authorization & resource servers, a typed client, Hono adapters (including a backend-for-frontend), and React bindings.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"authorization-server",
|