@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 +5 -5
- package/dist/errors.js +6 -9
- package/dist/server/_error-response.d.ts +11 -0
- package/dist/server/_error-response.js +33 -0
- package/dist/server/authorization-server.js +19 -11
- package/dist/server/resource-server.d.ts +19 -7
- package/dist/server/resource-server.js +5 -26
- package/package.json +1 -1
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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
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
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* `
|
|
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
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
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.
|
|
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",
|