@udibo/oauth2 0.18.0 → 0.19.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/dist/errors.d.ts CHANGED
@@ -64,11 +64,11 @@ export declare function isOAuth2Error(value: unknown): value is OAuth2Error;
64
64
  * anything else is wrapped in a {@link ServerError} with the original error
65
65
  * attached as `cause`.
66
66
  *
67
- * Either way, `type` and `extensions.error_uri` are synced so that if one is
68
- * set the other is too — **mutating an OAuth2Error input in place**. This lets
69
- * both the standard OAuth2 error format (which emits `error_uri`) and the
70
- * RFC 9457 Problem Details format (which emits `type`) carry the same URI
71
- * without the handler having to set both fields.
67
+ * An error that sets `extensions.error_uri` but no `type` gets `type` set to
68
+ * the same URI — **mutating an OAuth2Error input in place** — so the RFC 9457
69
+ * Problem Details format, which emits `type`, carries it. `type` is never
70
+ * copied into `extensions.error_uri`, so a problem-details body does not repeat
71
+ * it; the RFC 6749 error format emits `error_uri` from either field.
72
72
  */
73
73
  export declare function toOAuth2Error(error: unknown): OAuth2Error;
74
74
  /**
package/dist/errors.js CHANGED
@@ -38,20 +38,17 @@ export function isOAuth2Error(value) {
38
38
  * anything else is wrapped in a {@link ServerError} with the original error
39
39
  * attached as `cause`.
40
40
  *
41
- * Either way, `type` and `extensions.error_uri` are synced so that if one is
42
- * set the other is too — **mutating an OAuth2Error input in place**. This lets
43
- * both the standard OAuth2 error format (which emits `error_uri`) and the
44
- * RFC 9457 Problem Details format (which emits `type`) carry the same URI
45
- * without the handler having to set both fields.
41
+ * An error that sets `extensions.error_uri` but no `type` gets `type` set to
42
+ * the same URI — **mutating an OAuth2Error input in place** — so the RFC 9457
43
+ * Problem Details format, which emits `type`, carries it. `type` is never
44
+ * copied into `extensions.error_uri`, so a problem-details body does not repeat
45
+ * it; the RFC 6749 error format emits `error_uri` from either field.
46
46
  */
47
47
  export function toOAuth2Error(error) {
48
48
  const oauth2Error = isOAuth2Error(error)
49
49
  ? error
50
50
  : new ServerError("unexpected error", { cause: error });
51
- if (oauth2Error.type && !oauth2Error.extensions.error_uri) {
52
- oauth2Error.extensions.error_uri = oauth2Error.type;
53
- }
54
- else if (oauth2Error.extensions.error_uri && !oauth2Error.type) {
51
+ if (oauth2Error.extensions.error_uri && !oauth2Error.type) {
55
52
  oauth2Error.type = oauth2Error.extensions.error_uri;
56
53
  }
57
54
  return oauth2Error;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Serializes OAuth2 errors into responses for the resource and authorization
3
+ * servers.
4
+ *
5
+ * @module
6
+ */
7
+ import type { OAuth2Error } from "../errors.ts";
8
+ /** Copies the error's headers onto the response, except `Content-Type`, which the serializer owns. */
9
+ export declare function copyErrorHeaders(oauth2Error: OAuth2Error, response: Response): Response;
10
+ /** The RFC 6749 §5.2 error response: `{ error, error_description?, error_uri? }` as `application/json`, carrying the error's headers. */
11
+ export declare function oauth2ErrorResponse(oauth2Error: OAuth2Error): Response;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Serializes OAuth2 errors into responses for the resource and authorization
3
+ * servers.
4
+ *
5
+ * @module
6
+ */
7
+ /** Copies the error's headers onto the response, except `Content-Type`, which the serializer owns. */
8
+ export function copyErrorHeaders(oauth2Error, response) {
9
+ for (const [key, value] of oauth2Error.headers) {
10
+ if (key.toLowerCase() === "content-type")
11
+ continue;
12
+ response.headers.set(key, value);
13
+ }
14
+ return response;
15
+ }
16
+ /** The RFC 6749 §5.2 error response: `{ error, error_description?, error_uri? }` as `application/json`, carrying the error's headers. */
17
+ export function oauth2ErrorResponse(oauth2Error) {
18
+ const body = {
19
+ error: oauth2Error.extensions.error ?? "server_error",
20
+ };
21
+ const description = oauth2Error.extensions.error_description ?? oauth2Error.exposedMessage;
22
+ if (description) {
23
+ body.error_description = description;
24
+ }
25
+ const errorUri = oauth2Error.extensions.error_uri ?? oauth2Error.type;
26
+ if (errorUri) {
27
+ body.error_uri = errorUri;
28
+ }
29
+ return copyErrorHeaders(oauth2Error, new Response(JSON.stringify(body), {
30
+ status: oauth2Error.status,
31
+ headers: { "Content-Type": "application/json;charset=UTF-8" },
32
+ }));
33
+ }
@@ -20,7 +20,7 @@
20
20
  * @module
21
21
  */
22
22
  import { snapshotAuthenticationContext } from "../utils/authentication-context.js";
23
- import { AccessDeniedError, InvalidRequestError, InvalidTokenError, isOAuth2Error, OAuth2Error, ServerError, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, } from "../errors.js";
23
+ import { AccessDeniedError, InvalidRequestError, InvalidTokenError, isOAuth2Error, OAuth2Error, ServerError, toOAuth2Error, UnauthorizedClientError, UnsupportedGrantTypeError, UnsupportedResponseTypeError, } from "../errors.js";
24
24
  import { authenticateClientCredentials, extractClientCredentials, } from "./client-authentication.js";
25
25
  import { isRedirectUriPattern, matchRedirectUri } from "./redirect-uri.js";
26
26
  import { AuthorizationCodeGrant } from "./grants/authorization-code.js";
@@ -28,6 +28,7 @@ import { DeviceAuthorizationGrant } from "./grants/device-authorization.js";
28
28
  import { ResourceServer, } from "./resource-server.js";
29
29
  import { defaultSubjectOf, signJwt, verifyJwt, } from "./signing-keys.js";
30
30
  import { validateCodeChallenge } from "../utils/pkce.js";
31
+ import { oauth2ErrorResponse } from "./_error-response.js";
31
32
  import { assertMaxBodyBytes, DEFAULT_MAX_BODY_BYTES, readBoundedBody, } from "../utils/_body.js";
32
33
  const ENDPOINTS = {
33
34
  authorization: {
@@ -237,6 +238,13 @@ export class AuthorizationServer extends ResourceServer {
237
238
  this.#introspectionClaims = options.introspectionClaims;
238
239
  this.#canIntrospectToken = options.canIntrospectToken;
239
240
  }
241
+ #handleEndpointError(error, prepare) {
242
+ const oauth2Error = toOAuth2Error(error);
243
+ prepare?.(oauth2Error);
244
+ if (this.throwOnError)
245
+ throw oauth2Error;
246
+ return oauth2ErrorResponse(oauth2Error);
247
+ }
240
248
  /**
241
249
  * Whether this instance will actually serve the OIDC surface: `true` once
242
250
  * `signingKeys` is set, which is what gates `id_token` issuance and the
@@ -441,7 +449,7 @@ export class AuthorizationServer extends ResourceServer {
441
449
  return await this.createTokenResponse(token, context, grant);
442
450
  }
443
451
  catch (error) {
444
- return this.handleError(error, this.prepareClientAuthError(request));
452
+ return this.#handleEndpointError(error, this.prepareClientAuthError(request));
445
453
  }
446
454
  }
447
455
  /**
@@ -485,7 +493,7 @@ export class AuthorizationServer extends ResourceServer {
485
493
  */
486
494
  async handleOidcMetadataRequest(request) {
487
495
  if (!this.signingKeys) {
488
- return this.handleError(new OAuth2Error(404, "OIDC issuance is not enabled", {
496
+ return this.#handleEndpointError(new OAuth2Error(404, "OIDC issuance is not enabled", {
489
497
  extensions: { error: "not_found" },
490
498
  }));
491
499
  }
@@ -497,7 +505,7 @@ export class AuthorizationServer extends ResourceServer {
497
505
  */
498
506
  async handleJwksRequest(_request) {
499
507
  if (!this.signingKeys) {
500
- return this.handleError(new OAuth2Error(404, "OIDC issuance is not enabled", {
508
+ return this.#handleEndpointError(new OAuth2Error(404, "OIDC issuance is not enabled", {
501
509
  extensions: { error: "not_found" },
502
510
  }));
503
511
  }
@@ -512,7 +520,7 @@ export class AuthorizationServer extends ResourceServer {
512
520
  });
513
521
  }
514
522
  catch (error) {
515
- return this.handleError(error);
523
+ return this.#handleEndpointError(error);
516
524
  }
517
525
  }
518
526
  /**
@@ -626,7 +634,7 @@ export class AuthorizationServer extends ResourceServer {
626
634
  return new Response(null, { status: 302, headers });
627
635
  }
628
636
  catch (error) {
629
- return this.handleError(error);
637
+ return this.#handleEndpointError(error);
630
638
  }
631
639
  }
632
640
  // RP-Initiated Logout §2: parameters come from the GET query or POST body.
@@ -799,7 +807,7 @@ export class AuthorizationServer extends ResourceServer {
799
807
  }
800
808
  #authorizeErrorResponse(error, redirectUrl) {
801
809
  if (!redirectUrl || !(error instanceof Error) || !isOAuth2Error(error)) {
802
- return this.handleError(error);
810
+ return this.#handleEndpointError(error);
803
811
  }
804
812
  const { searchParams } = redirectUrl;
805
813
  searchParams.set("error", error.extensions.error ?? "server_error");
@@ -958,7 +966,7 @@ export class AuthorizationServer extends ResourceServer {
958
966
  return new Response(null, { status: 200 });
959
967
  }
960
968
  catch (error) {
961
- return this.handleError(error, this.prepareClientAuthError(request));
969
+ return this.#handleEndpointError(error, this.prepareClientAuthError(request));
962
970
  }
963
971
  }
964
972
  async #resolveToken(tokenService, value, hint) {
@@ -1086,7 +1094,7 @@ export class AuthorizationServer extends ResourceServer {
1086
1094
  });
1087
1095
  }
1088
1096
  catch (error) {
1089
- return this.handleError(error, this.prepareClientAuthError(request));
1097
+ return this.#handleEndpointError(error, this.prepareClientAuthError(request));
1090
1098
  }
1091
1099
  }
1092
1100
  /**
@@ -1189,7 +1197,7 @@ export class AuthorizationServer extends ResourceServer {
1189
1197
  });
1190
1198
  }
1191
1199
  catch (error) {
1192
- return this.handleError(error);
1200
+ return this.#handleEndpointError(error);
1193
1201
  }
1194
1202
  }
1195
1203
  /**
@@ -1244,7 +1252,7 @@ export class AuthorizationServer extends ResourceServer {
1244
1252
  });
1245
1253
  }
1246
1254
  catch (error) {
1247
- return this.handleError(error, this.prepareClientAuthError(request));
1255
+ return this.#handleEndpointError(error, this.prepareClientAuthError(request));
1248
1256
  }
1249
1257
  }
1250
1258
  }
@@ -77,12 +77,24 @@ export interface ResourceServerOptions<Client extends ClientInterface, User, S e
77
77
  */
78
78
  maxTokenBodyBytes?: number;
79
79
  /**
80
- * Format for error responses.
81
- * - `"oauth2"` (default): Standard OAuth2 error format with `error`,
82
- * `error_description`, and `error_uri` fields.
83
- * - `"problem-details"`: RFC 9457 Problem Details format with OAuth2
84
- * extensions. `type` and `error_uri` carry the same URI; `detail` and
85
- * `error_description` carry the same message.
80
+ * Format for protected-resource refusals: the responses from `protect()`,
81
+ * {@linkcode ResourceServer.handleAuthError} and
82
+ * {@linkcode ResourceServer.createErrorResponse}, including an
83
+ * authorization server's own protected resources such as UserInfo.
84
+ * - `"oauth2"` (default): `{ error, error_description?, error_uri? }` as
85
+ * `application/json`.
86
+ * - `"problem-details"`: RFC 9457 Problem Details as
87
+ * `application/problem+json`: `status`, `title`, `detail` (the message),
88
+ * `type` when set, the OAuth2 `error` code and any other extension members
89
+ * such as `requiredScope`. `error_description` and `error_uri` appear only
90
+ * when the error sets them as extensions.
91
+ *
92
+ * Either way, a 401 or 403 from `protect()` or `handleAuthError` carries the
93
+ * `error` code and `error_description` in its `WWW-Authenticate` challenge.
94
+ *
95
+ * An authorization server's OAuth endpoints (token, revocation,
96
+ * introspection, device authorization, authorize, end-session, metadata and
97
+ * JWKS) ignore it and always answer in the RFC 6749 §5.2 format.
86
98
  */
87
99
  errorFormat?: "oauth2" | "problem-details";
88
100
  /**
@@ -160,7 +172,7 @@ export declare class ResourceServer<Client extends ClientInterface, User, S exte
160
172
  Scope: ScopeConstructor<S>;
161
173
  /** Protection space named in `WWW-Authenticate` challenges. */
162
174
  realm: string;
163
- /** Body format used for error responses: OAuth2 or RFC 9457 problem details. */
175
+ /** Body format of protected-resource refusals: OAuth2 or RFC 9457 problem details. See {@linkcode ResourceServerOptions.errorFormat}. */
164
176
  errorFormat: "oauth2" | "problem-details";
165
177
  /** When true, handler boundaries rethrow {@linkcode OAuth2Error} instead of returning a response. */
166
178
  throwOnError: boolean;
@@ -9,6 +9,7 @@
9
9
  import { BasicScope } from "../models/scope.js";
10
10
  import { AccessDeniedError, InsufficientPermissionsError, InsufficientScopeError, InvalidTokenError, toOAuth2Error, } from "../errors.js";
11
11
  import { authorizationFromClaims, } from "../models/authorization.js";
12
+ import { copyErrorHeaders, oauth2ErrorResponse } from "./_error-response.js";
12
13
  import { assertClockSkewSeconds } from "./_clock-skew.js";
13
14
  import { assertMaxBodyBytes, DEFAULT_MAX_TOKEN_BODY_BYTES, peekBoundedBody, } from "../utils/_body.js";
14
15
  /** Bearer token regex pattern. */
@@ -74,7 +75,7 @@ export class ResourceServer {
74
75
  Scope;
75
76
  /** Protection space named in `WWW-Authenticate` challenges. */
76
77
  realm;
77
- /** Body format used for error responses: OAuth2 or RFC 9457 problem details. */
78
+ /** Body format of protected-resource refusals: OAuth2 or RFC 9457 problem details. See {@linkcode ResourceServerOptions.errorFormat}. */
78
79
  errorFormat;
79
80
  /** When true, handler boundaries rethrow {@linkcode OAuth2Error} instead of returning a response. */
80
81
  throwOnError;
@@ -179,32 +180,10 @@ export class ResourceServer {
179
180
  */
180
181
  createErrorResponse(error) {
181
182
  const oauth2Error = toOAuth2Error(error);
182
- const response = this.errorFormat === "problem-details"
183
- ? oauth2Error.getResponse()
184
- : this.#createOAuth2ErrorResponse(oauth2Error);
185
- for (const [key, value] of oauth2Error.headers) {
186
- if (key.toLowerCase() === "content-type")
187
- continue;
188
- response.headers.set(key, value);
183
+ if (this.errorFormat !== "problem-details") {
184
+ return oauth2ErrorResponse(oauth2Error);
189
185
  }
190
- return response;
191
- }
192
- #createOAuth2ErrorResponse(oauth2Error) {
193
- const body = {
194
- error: oauth2Error.extensions.error ?? "server_error",
195
- };
196
- const description = oauth2Error.extensions.error_description ?? oauth2Error.exposedMessage;
197
- if (description) {
198
- body.error_description = description;
199
- }
200
- const errorUri = oauth2Error.extensions.error_uri ?? oauth2Error.type;
201
- if (errorUri) {
202
- body.error_uri = errorUri;
203
- }
204
- return new Response(JSON.stringify(body), {
205
- status: oauth2Error.status,
206
- headers: { "Content-Type": "application/json;charset=UTF-8" },
207
- });
186
+ return copyErrorHeaders(oauth2Error, oauth2Error.getResponse());
208
187
  }
209
188
  /**
210
189
  * Normalizes an error at a handler boundary and either rethrows it (when
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udibo/oauth2",
3
- "version": "0.18.0",
3
+ "version": "0.19.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",