@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 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 UserInfo. The
93
- server does not act on `prompt` or `max_age`.
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? }` — user is authenticated;
51
- * the authorize flow continues. Supply only verified event claims in
52
- * `authenticationContext`; the code and its tokens retain that snapshot.
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
  };
@@ -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
- async function currentUser(state, request) {
193
+ function currentSession(state, request) {
185
194
  const sessionId = readCookie(request, SESSION_COOKIE);
186
- if (!sessionId)
187
- return undefined;
188
- const userId = state.sessions.get(sessionId);
189
- if (!userId)
190
- return undefined;
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 user = await currentUser(state, request);
196
- if (user)
197
- return { user };
198
- return htmlResponse(loginPage({ returnTo: pathAndQuery(request), users: config.users }));
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; application policy must validate and honor them. */
41
+ /** Raw space-delimited ACR preferences; `authenticateUser` receives them validated. */
42
42
  acrValues?: string;
43
- /** Raw maximum authentication age in seconds; application policy must validate and enforce it. */
43
+ /** Raw maximum authentication age in seconds; the authorize endpoint validates and enforces it. */
44
44
  maxAge?: string;
45
- /** Raw prompt values; application policy must honor or refuse every supported control. */
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 authResult = await authenticateUser(request);
789
- if (authResult instanceof Response)
790
- return authResult;
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 needsConsent = scope !== undefined && !(authorizedScope && authorizedScope.has(scope));
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
- return consent;
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";
@@ -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";
@@ -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.26.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",