@udibo/oauth2 0.26.0 → 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/hono/authorization-server.d.ts +21 -8
  3. package/dist/adapters/hono/authorization-server.js +2 -2
  4. package/dist/adapters/hono/bff/bff.d.ts +18 -4
  5. package/dist/adapters/hono/bff/bff.js +69 -3
  6. package/dist/cli/idp/server.js +35 -14
  7. package/dist/client/_http.d.ts +6 -1
  8. package/dist/client/_http.js +15 -2
  9. package/dist/client/_issuer-refusal.d.ts +7 -0
  10. package/dist/client/_issuer-refusal.js +22 -0
  11. package/dist/client/direct-client.d.ts +2 -1
  12. package/dist/client/direct-client.js +9 -22
  13. package/dist/errors.d.ts +34 -0
  14. package/dist/errors.js +58 -0
  15. package/dist/identity/external/_shared.d.ts +18 -0
  16. package/dist/identity/external/_shared.js +15 -0
  17. package/dist/identity/external/apple.d.ts +3 -0
  18. package/dist/identity/external/apple.js +10 -1
  19. package/dist/identity/external/errors.d.ts +3 -1
  20. package/dist/identity/external/flow.d.ts +4 -2
  21. package/dist/identity/external/flow.js +10 -2
  22. package/dist/identity/external/oauth2.d.ts +19 -1
  23. package/dist/identity/external/oauth2.js +21 -2
  24. package/dist/identity/external/oidc.d.ts +6 -0
  25. package/dist/identity/external/oidc.js +13 -1
  26. package/dist/identity/external/provider.d.ts +12 -2
  27. package/dist/models/authentication.d.ts +28 -0
  28. package/dist/models/authorization-code.d.ts +3 -3
  29. package/dist/models/responses.d.ts +8 -2
  30. package/dist/server/authentication-request.d.ts +60 -0
  31. package/dist/server/authentication-request.js +102 -0
  32. package/dist/server/authorization-server.d.ts +55 -7
  33. package/dist/server/authorization-server.js +54 -11
  34. package/dist/server/mod.d.ts +3 -2
  35. package/dist/server/mod.js +2 -1
  36. package/package.json +1 -1
@@ -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.28.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",