@udibo/oauth2 0.18.0 → 0.20.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/_challenge.d.ts +18 -0
- package/dist/server/_challenge.js +41 -0
- package/dist/server/_error-response.d.ts +11 -0
- package/dist/server/_error-response.js +33 -0
- package/dist/server/authorization-server.d.ts +2 -0
- package/dist/server/authorization-server.js +23 -12
- package/dist/server/resource-server.d.ts +47 -10
- package/dist/server/resource-server.js +44 -46
- 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,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character-set rules for `WWW-Authenticate` challenge parameters (RFC 7235
|
|
3
|
+
* §2.2, RFC 9110 §5.6.4, RFC 6750 §3).
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
/** Throws `TypeError` unless `realm` is printable ASCII (U+0020–U+007E), the only text every HTTP stack carries in a header unaltered. */
|
|
8
|
+
export declare function assertRealm(realm: string): void;
|
|
9
|
+
/** The `realm` parameter as a quoted-string; `realm` must already satisfy {@linkcode assertRealm}. */
|
|
10
|
+
export declare function quoteRealm(realm: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Maps text into the RFC 6750 §3 `error_description` set
|
|
13
|
+
* (`%x20-21 / %x23-5B / %x5D-7E`): control characters are dropped, `"`
|
|
14
|
+
* becomes `'`, and every other code point outside the set becomes `?`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toChallengeDescription(text: string): string;
|
|
17
|
+
/** `scope` with its space-separated tokens rejoined by single spaces, or `undefined` unless every token is an RFC 6750 §3 scope-token. */
|
|
18
|
+
export declare function toChallengeScope(scope: string): string | undefined;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character-set rules for `WWW-Authenticate` challenge parameters (RFC 7235
|
|
3
|
+
* §2.2, RFC 9110 §5.6.4, RFC 6750 §3).
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { NQCHAR } from "../models/scope.js";
|
|
8
|
+
const PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
|
|
9
|
+
// oxlint-disable-next-line no-control-regex
|
|
10
|
+
const CONTROL_CHARACTERS = /[\x00-\x1f\x7f]/g;
|
|
11
|
+
const OUTSIDE_DESCRIPTION_SET = /[^\x20\x21\x23-\x5b\x5d-\x7e]/gu;
|
|
12
|
+
const SCOPE_TOKEN_ONLY = new RegExp(`^${NQCHAR.source}+$`);
|
|
13
|
+
/** Throws `TypeError` unless `realm` is printable ASCII (U+0020–U+007E), the only text every HTTP stack carries in a header unaltered. */
|
|
14
|
+
export function assertRealm(realm) {
|
|
15
|
+
if (typeof realm !== "string" || !PRINTABLE_ASCII.test(realm)) {
|
|
16
|
+
throw new TypeError(`realm must contain only printable ASCII characters (U+0020 to U+007E), got ${JSON.stringify(realm)}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/** The `realm` parameter as a quoted-string; `realm` must already satisfy {@linkcode assertRealm}. */
|
|
20
|
+
export function quoteRealm(realm) {
|
|
21
|
+
return `"${realm.replace(/["\\]/g, "\\$&")}"`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Maps text into the RFC 6750 §3 `error_description` set
|
|
25
|
+
* (`%x20-21 / %x23-5B / %x5D-7E`): control characters are dropped, `"`
|
|
26
|
+
* becomes `'`, and every other code point outside the set becomes `?`.
|
|
27
|
+
*/
|
|
28
|
+
export function toChallengeDescription(text) {
|
|
29
|
+
return text
|
|
30
|
+
.replace(CONTROL_CHARACTERS, "")
|
|
31
|
+
.replaceAll('"', "'")
|
|
32
|
+
.replace(OUTSIDE_DESCRIPTION_SET, "?");
|
|
33
|
+
}
|
|
34
|
+
/** `scope` with its space-separated tokens rejoined by single spaces, or `undefined` unless every token is an RFC 6750 §3 scope-token. */
|
|
35
|
+
export function toChallengeScope(scope) {
|
|
36
|
+
const tokens = scope.split(" ").filter((token) => token !== "");
|
|
37
|
+
return tokens.length > 0 &&
|
|
38
|
+
tokens.every((token) => SCOPE_TOKEN_ONLY.test(token))
|
|
39
|
+
? tokens.join(" ")
|
|
40
|
+
: undefined;
|
|
41
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -446,6 +446,8 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
|
|
|
446
446
|
* @throws {Error} If `requireOidc` is set without `signingKeys`.
|
|
447
447
|
* @throws {RangeError} If `maxBodyBytes` or `maxTokenBodyBytes` is not a
|
|
448
448
|
* positive integer.
|
|
449
|
+
* @throws {TypeError} If `realm` contains a character outside printable
|
|
450
|
+
* ASCII.
|
|
449
451
|
*/
|
|
450
452
|
constructor(options: AuthorizationServerOptions<Client, User, S>);
|
|
451
453
|
/**
|
|
@@ -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,8 @@ 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";
|
|
32
|
+
import { quoteRealm } from "./_challenge.js";
|
|
31
33
|
import { assertMaxBodyBytes, DEFAULT_MAX_BODY_BYTES, readBoundedBody, } from "../utils/_body.js";
|
|
32
34
|
const ENDPOINTS = {
|
|
33
35
|
authorization: {
|
|
@@ -199,6 +201,8 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
199
201
|
* @throws {Error} If `requireOidc` is set without `signingKeys`.
|
|
200
202
|
* @throws {RangeError} If `maxBodyBytes` or `maxTokenBodyBytes` is not a
|
|
201
203
|
* positive integer.
|
|
204
|
+
* @throws {TypeError} If `realm` contains a character outside printable
|
|
205
|
+
* ASCII.
|
|
202
206
|
*/
|
|
203
207
|
constructor(options) {
|
|
204
208
|
super({
|
|
@@ -237,6 +241,13 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
237
241
|
this.#introspectionClaims = options.introspectionClaims;
|
|
238
242
|
this.#canIntrospectToken = options.canIntrospectToken;
|
|
239
243
|
}
|
|
244
|
+
#handleEndpointError(error, prepare) {
|
|
245
|
+
const oauth2Error = toOAuth2Error(error);
|
|
246
|
+
prepare?.(oauth2Error);
|
|
247
|
+
if (this.throwOnError)
|
|
248
|
+
throw oauth2Error;
|
|
249
|
+
return oauth2ErrorResponse(oauth2Error);
|
|
250
|
+
}
|
|
240
251
|
/**
|
|
241
252
|
* Whether this instance will actually serve the OIDC surface: `true` once
|
|
242
253
|
* `signingKeys` is set, which is what gates `id_token` issuance and the
|
|
@@ -328,7 +339,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
328
339
|
return (error) => {
|
|
329
340
|
this.addCacheHeaders(error);
|
|
330
341
|
if (error.status === 401 && request.headers.get("authorization")) {
|
|
331
|
-
error.headers.set("WWW-Authenticate", `Basic realm
|
|
342
|
+
error.headers.set("WWW-Authenticate", `Basic realm=${quoteRealm(this.realm)}`);
|
|
332
343
|
}
|
|
333
344
|
};
|
|
334
345
|
}
|
|
@@ -441,7 +452,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
441
452
|
return await this.createTokenResponse(token, context, grant);
|
|
442
453
|
}
|
|
443
454
|
catch (error) {
|
|
444
|
-
return this
|
|
455
|
+
return this.#handleEndpointError(error, this.prepareClientAuthError(request));
|
|
445
456
|
}
|
|
446
457
|
}
|
|
447
458
|
/**
|
|
@@ -485,7 +496,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
485
496
|
*/
|
|
486
497
|
async handleOidcMetadataRequest(request) {
|
|
487
498
|
if (!this.signingKeys) {
|
|
488
|
-
return this
|
|
499
|
+
return this.#handleEndpointError(new OAuth2Error(404, "OIDC issuance is not enabled", {
|
|
489
500
|
extensions: { error: "not_found" },
|
|
490
501
|
}));
|
|
491
502
|
}
|
|
@@ -497,7 +508,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
497
508
|
*/
|
|
498
509
|
async handleJwksRequest(_request) {
|
|
499
510
|
if (!this.signingKeys) {
|
|
500
|
-
return this
|
|
511
|
+
return this.#handleEndpointError(new OAuth2Error(404, "OIDC issuance is not enabled", {
|
|
501
512
|
extensions: { error: "not_found" },
|
|
502
513
|
}));
|
|
503
514
|
}
|
|
@@ -512,7 +523,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
512
523
|
});
|
|
513
524
|
}
|
|
514
525
|
catch (error) {
|
|
515
|
-
return this
|
|
526
|
+
return this.#handleEndpointError(error);
|
|
516
527
|
}
|
|
517
528
|
}
|
|
518
529
|
/**
|
|
@@ -626,7 +637,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
626
637
|
return new Response(null, { status: 302, headers });
|
|
627
638
|
}
|
|
628
639
|
catch (error) {
|
|
629
|
-
return this
|
|
640
|
+
return this.#handleEndpointError(error);
|
|
630
641
|
}
|
|
631
642
|
}
|
|
632
643
|
// RP-Initiated Logout §2: parameters come from the GET query or POST body.
|
|
@@ -799,7 +810,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
799
810
|
}
|
|
800
811
|
#authorizeErrorResponse(error, redirectUrl) {
|
|
801
812
|
if (!redirectUrl || !(error instanceof Error) || !isOAuth2Error(error)) {
|
|
802
|
-
return this
|
|
813
|
+
return this.#handleEndpointError(error);
|
|
803
814
|
}
|
|
804
815
|
const { searchParams } = redirectUrl;
|
|
805
816
|
searchParams.set("error", error.extensions.error ?? "server_error");
|
|
@@ -958,7 +969,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
958
969
|
return new Response(null, { status: 200 });
|
|
959
970
|
}
|
|
960
971
|
catch (error) {
|
|
961
|
-
return this
|
|
972
|
+
return this.#handleEndpointError(error, this.prepareClientAuthError(request));
|
|
962
973
|
}
|
|
963
974
|
}
|
|
964
975
|
async #resolveToken(tokenService, value, hint) {
|
|
@@ -1086,7 +1097,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1086
1097
|
});
|
|
1087
1098
|
}
|
|
1088
1099
|
catch (error) {
|
|
1089
|
-
return this
|
|
1100
|
+
return this.#handleEndpointError(error, this.prepareClientAuthError(request));
|
|
1090
1101
|
}
|
|
1091
1102
|
}
|
|
1092
1103
|
/**
|
|
@@ -1189,7 +1200,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1189
1200
|
});
|
|
1190
1201
|
}
|
|
1191
1202
|
catch (error) {
|
|
1192
|
-
return this
|
|
1203
|
+
return this.#handleEndpointError(error);
|
|
1193
1204
|
}
|
|
1194
1205
|
}
|
|
1195
1206
|
/**
|
|
@@ -1244,7 +1255,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1244
1255
|
});
|
|
1245
1256
|
}
|
|
1246
1257
|
catch (error) {
|
|
1247
|
-
return this
|
|
1258
|
+
return this.#handleEndpointError(error, this.prepareClientAuthError(request));
|
|
1248
1259
|
}
|
|
1249
1260
|
}
|
|
1250
1261
|
}
|
|
@@ -37,7 +37,14 @@ export interface ResourceServerOptions<Client extends ClientInterface, User, S e
|
|
|
37
37
|
resolve: (request: Request) => ResourceServerContext<Client, User, S> | Promise<ResourceServerContext<Client, User, S>>;
|
|
38
38
|
/** Scope constructor used to parse and compare scopes. Defaults to {@linkcode BasicScope}. */
|
|
39
39
|
Scope?: ScopeConstructor<S>;
|
|
40
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Protection space named in `WWW-Authenticate` challenges. Defaults to
|
|
42
|
+
* `"Service"`. Must be printable ASCII (U+0020 to U+007E); a product name
|
|
43
|
+
* with other characters needs an ASCII form here.
|
|
44
|
+
*
|
|
45
|
+
* @throws {TypeError} From the constructor when it contains any other
|
|
46
|
+
* character.
|
|
47
|
+
*/
|
|
41
48
|
realm?: string;
|
|
42
49
|
/**
|
|
43
50
|
* Leeway in seconds applied to the access token's expiry, to absorb clock
|
|
@@ -77,12 +84,24 @@ export interface ResourceServerOptions<Client extends ClientInterface, User, S e
|
|
|
77
84
|
*/
|
|
78
85
|
maxTokenBodyBytes?: number;
|
|
79
86
|
/**
|
|
80
|
-
* Format for
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* `
|
|
87
|
+
* Format for protected-resource refusals: the responses from `protect()`,
|
|
88
|
+
* {@linkcode ResourceServer.handleAuthError} and
|
|
89
|
+
* {@linkcode ResourceServer.createErrorResponse}, including an
|
|
90
|
+
* authorization server's own protected resources such as UserInfo.
|
|
91
|
+
* - `"oauth2"` (default): `{ error, error_description?, error_uri? }` as
|
|
92
|
+
* `application/json`.
|
|
93
|
+
* - `"problem-details"`: RFC 9457 Problem Details as
|
|
94
|
+
* `application/problem+json`: `status`, `title`, `detail` (the message),
|
|
95
|
+
* `type` when set, the OAuth2 `error` code and any other extension members
|
|
96
|
+
* such as `requiredScope`. `error_description` and `error_uri` appear only
|
|
97
|
+
* when the error sets them as extensions.
|
|
98
|
+
*
|
|
99
|
+
* Either way, a 401 or 403 from `protect()` or `handleAuthError` carries the
|
|
100
|
+
* `error` code and `error_description` in its `WWW-Authenticate` challenge.
|
|
101
|
+
*
|
|
102
|
+
* An authorization server's OAuth endpoints (token, revocation,
|
|
103
|
+
* introspection, device authorization, authorize, end-session, metadata and
|
|
104
|
+
* JWKS) ignore it and always answer in the RFC 6749 §5.2 format.
|
|
86
105
|
*/
|
|
87
106
|
errorFormat?: "oauth2" | "problem-details";
|
|
88
107
|
/**
|
|
@@ -158,14 +177,22 @@ export declare class ResourceServer<Client extends ClientInterface, User, S exte
|
|
|
158
177
|
#private;
|
|
159
178
|
/** Scope constructor used to parse and compare scopes. */
|
|
160
179
|
Scope: ScopeConstructor<S>;
|
|
161
|
-
/**
|
|
162
|
-
realm: string;
|
|
163
|
-
/** Body format used for error responses: OAuth2 or RFC 9457 problem details. */
|
|
180
|
+
/** Body format of protected-resource refusals: OAuth2 or RFC 9457 problem details. See {@linkcode ResourceServerOptions.errorFormat}. */
|
|
164
181
|
errorFormat: "oauth2" | "problem-details";
|
|
165
182
|
/** When true, handler boundaries rethrow {@linkcode OAuth2Error} instead of returning a response. */
|
|
166
183
|
throwOnError: boolean;
|
|
167
184
|
/** Resolves the per-request {@link ResourceServerContext}. */
|
|
168
185
|
protected resolveContext: (request: Request) => ResourceServerContext<Client, User, S> | Promise<ResourceServerContext<Client, User, S>>;
|
|
186
|
+
/** Protection space named in `WWW-Authenticate` challenges. */
|
|
187
|
+
get realm(): string;
|
|
188
|
+
/**
|
|
189
|
+
* Sets the protection space, preserving the previous value if validation
|
|
190
|
+
* fails.
|
|
191
|
+
*
|
|
192
|
+
* @throws {TypeError} If the value contains a character outside printable
|
|
193
|
+
* ASCII (U+0020 to U+007E).
|
|
194
|
+
*/
|
|
195
|
+
set realm(value: string);
|
|
169
196
|
/**
|
|
170
197
|
* Leeway in seconds applied to the access token's expiry. Defaults to `0`.
|
|
171
198
|
* May be reassigned to any non-negative finite number with a finite
|
|
@@ -185,6 +212,8 @@ export declare class ResourceServer<Client extends ClientInterface, User, S exte
|
|
|
185
212
|
* @throws {RangeError} If `clockSkewSeconds` is negative, non-finite, or
|
|
186
213
|
* overflows when converted to milliseconds, or if `maxTokenBodyBytes` is
|
|
187
214
|
* not a positive integer.
|
|
215
|
+
* @throws {TypeError} If `realm` contains a character outside printable
|
|
216
|
+
* ASCII.
|
|
188
217
|
*/
|
|
189
218
|
constructor(options: ResourceServerOptions<Client, User, S>);
|
|
190
219
|
/**
|
|
@@ -205,6 +234,14 @@ export declare class ResourceServer<Client extends ClientInterface, User, S exte
|
|
|
205
234
|
* {@linkcode InsufficientScopeError} is the one that tells it "your token is
|
|
206
235
|
* fine, it just lacks scope" rather than sending it off to refresh.
|
|
207
236
|
*
|
|
237
|
+
* The result is always printable ASCII, so it never makes `Headers` throw.
|
|
238
|
+
* `error_description` is the same text the oauth2 response body uses (the
|
|
239
|
+
* `error_description` extension, else the error's exposed message, so an
|
|
240
|
+
* unexposed message stays out of both), narrowed to the RFC 6750 character
|
|
241
|
+
* set: control characters are dropped, `"` becomes `'`, and any other
|
|
242
|
+
* character outside the set becomes `?`. `scope` is omitted entirely when
|
|
243
|
+
* any of its tokens falls outside the RFC 6750 set.
|
|
244
|
+
*
|
|
208
245
|
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
|
209
246
|
*/
|
|
210
247
|
buildWwwAuthenticate(error?: OAuth2Error, requiredScope?: string): string;
|
|
@@ -9,7 +9,9 @@
|
|
|
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";
|
|
14
|
+
import { assertRealm, quoteRealm, toChallengeDescription, toChallengeScope, } from "./_challenge.js";
|
|
13
15
|
import { assertMaxBodyBytes, DEFAULT_MAX_TOKEN_BODY_BYTES, peekBoundedBody, } from "../utils/_body.js";
|
|
14
16
|
/** Bearer token regex pattern. */
|
|
15
17
|
export const BEARER_TOKEN = /^ *(?:[Bb][Ee][Aa][Rr][Ee][Rr]) +([\w-.~+/]+=*) *$/;
|
|
@@ -72,9 +74,7 @@ const CHALLENGE_ERROR_CODES = new Set([
|
|
|
72
74
|
export class ResourceServer {
|
|
73
75
|
/** Scope constructor used to parse and compare scopes. */
|
|
74
76
|
Scope;
|
|
75
|
-
/**
|
|
76
|
-
realm;
|
|
77
|
-
/** Body format used for error responses: OAuth2 or RFC 9457 problem details. */
|
|
77
|
+
/** Body format of protected-resource refusals: OAuth2 or RFC 9457 problem details. See {@linkcode ResourceServerOptions.errorFormat}. */
|
|
78
78
|
errorFormat;
|
|
79
79
|
/** When true, handler boundaries rethrow {@linkcode OAuth2Error} instead of returning a response. */
|
|
80
80
|
throwOnError;
|
|
@@ -82,6 +82,22 @@ export class ResourceServer {
|
|
|
82
82
|
resolveContext;
|
|
83
83
|
#clockSkewSeconds = 0;
|
|
84
84
|
#maxTokenBodyBytes;
|
|
85
|
+
#realm = "Service";
|
|
86
|
+
/** Protection space named in `WWW-Authenticate` challenges. */
|
|
87
|
+
get realm() {
|
|
88
|
+
return this.#realm;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Sets the protection space, preserving the previous value if validation
|
|
92
|
+
* fails.
|
|
93
|
+
*
|
|
94
|
+
* @throws {TypeError} If the value contains a character outside printable
|
|
95
|
+
* ASCII (U+0020 to U+007E).
|
|
96
|
+
*/
|
|
97
|
+
set realm(value) {
|
|
98
|
+
assertRealm(value);
|
|
99
|
+
this.#realm = value;
|
|
100
|
+
}
|
|
85
101
|
/**
|
|
86
102
|
* Leeway in seconds applied to the access token's expiry. Defaults to `0`.
|
|
87
103
|
* May be reassigned to any non-negative finite number with a finite
|
|
@@ -106,6 +122,8 @@ export class ResourceServer {
|
|
|
106
122
|
* @throws {RangeError} If `clockSkewSeconds` is negative, non-finite, or
|
|
107
123
|
* overflows when converted to milliseconds, or if `maxTokenBodyBytes` is
|
|
108
124
|
* not a positive integer.
|
|
125
|
+
* @throws {TypeError} If `realm` contains a character outside printable
|
|
126
|
+
* ASCII.
|
|
109
127
|
*/
|
|
110
128
|
constructor(options) {
|
|
111
129
|
this.Scope =
|
|
@@ -137,31 +155,33 @@ export class ResourceServer {
|
|
|
137
155
|
* {@linkcode InsufficientScopeError} is the one that tells it "your token is
|
|
138
156
|
* fine, it just lacks scope" rather than sending it off to refresh.
|
|
139
157
|
*
|
|
158
|
+
* The result is always printable ASCII, so it never makes `Headers` throw.
|
|
159
|
+
* `error_description` is the same text the oauth2 response body uses (the
|
|
160
|
+
* `error_description` extension, else the error's exposed message, so an
|
|
161
|
+
* unexposed message stays out of both), narrowed to the RFC 6750 character
|
|
162
|
+
* set: control characters are dropped, `"` becomes `'`, and any other
|
|
163
|
+
* character outside the set becomes `?`. `scope` is omitted entirely when
|
|
164
|
+
* any of its tokens falls outside the RFC 6750 set.
|
|
165
|
+
*
|
|
140
166
|
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3
|
|
141
167
|
*/
|
|
142
168
|
buildWwwAuthenticate(error, requiredScope) {
|
|
143
|
-
const
|
|
144
|
-
// oxlint-disable-next-line no-control-regex
|
|
145
|
-
.replace(/[\x00-\x1f\x7f]/g, "")
|
|
146
|
-
.replace(/\\/g, "\\\\")
|
|
147
|
-
.replace(/"/g, '\\"');
|
|
148
|
-
const parts = [`realm="${quote(this.realm)}"`];
|
|
169
|
+
const parts = [`realm=${quoteRealm(this.realm)}`];
|
|
149
170
|
const code = error?.extensions.error;
|
|
150
171
|
if (error && code && CHALLENGE_ERROR_CODES.has(code)) {
|
|
151
|
-
parts.push(`error="${
|
|
152
|
-
|
|
153
|
-
|
|
172
|
+
parts.push(`error="${code}"`);
|
|
173
|
+
const description = toChallengeDescription(error.extensions.error_description ?? error.exposedMessage);
|
|
174
|
+
if (description) {
|
|
175
|
+
parts.push(`error_description="${description}"`);
|
|
154
176
|
}
|
|
155
177
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
parts.push(`scope="${quote(scope)}"`);
|
|
164
|
-
}
|
|
178
|
+
const scope = requiredScope ||
|
|
179
|
+
(error instanceof InsufficientScopeError
|
|
180
|
+
? error.extensions.requiredScope
|
|
181
|
+
: undefined);
|
|
182
|
+
const challengeScope = scope ? toChallengeScope(scope) : undefined;
|
|
183
|
+
if (challengeScope) {
|
|
184
|
+
parts.push(`scope="${challengeScope}"`);
|
|
165
185
|
}
|
|
166
186
|
return `Bearer ${parts.join(", ")}`;
|
|
167
187
|
}
|
|
@@ -179,32 +199,10 @@ export class ResourceServer {
|
|
|
179
199
|
*/
|
|
180
200
|
createErrorResponse(error) {
|
|
181
201
|
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);
|
|
189
|
-
}
|
|
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;
|
|
202
|
+
if (this.errorFormat !== "problem-details") {
|
|
203
|
+
return oauth2ErrorResponse(oauth2Error);
|
|
199
204
|
}
|
|
200
|
-
|
|
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
|
-
});
|
|
205
|
+
return copyErrorHeaders(oauth2Error, oauth2Error.getResponse());
|
|
208
206
|
}
|
|
209
207
|
/**
|
|
210
208
|
* 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.20.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",
|