@udibo/oauth2 0.12.0 → 0.12.2
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.
|
@@ -489,7 +489,8 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
|
|
|
489
489
|
* Supports multiple grant types based on the configured grants.
|
|
490
490
|
*
|
|
491
491
|
* A malformed request — wrong method, a content-type other than
|
|
492
|
-
* `application/x-www-form-urlencoded`,
|
|
492
|
+
* `application/x-www-form-urlencoded`, no `grant_type`, or a repeated
|
|
493
|
+
* recognized singleton form parameter — is a 400
|
|
493
494
|
* `invalid_request`. A `grant_type` with no registered grant is
|
|
494
495
|
* `unsupported_grant_type`, checked before client authentication; a client
|
|
495
496
|
* not registered for the grant is `unauthorized_client`. A body over
|
|
@@ -643,7 +644,8 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
|
|
|
643
644
|
* endpoint cannot be used as an oracle for whether a token value is live or
|
|
644
645
|
* whose it is. That covers the token only: a malformed request — wrong
|
|
645
646
|
* method, a content-type other than `application/x-www-form-urlencoded`, or
|
|
646
|
-
* no `token` parameter
|
|
647
|
+
* no `token` parameter, or repeated `token`, `token_type_hint`, `client_id` or
|
|
648
|
+
* `client_secret` — is a 400 `invalid_request`, as on the token
|
|
647
649
|
* endpoint.
|
|
648
650
|
*
|
|
649
651
|
* `token_type_hint` orders the lookup rather than restricting it, so a client
|
|
@@ -684,6 +686,9 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
|
|
|
684
686
|
* `application/x-www-form-urlencoded`, is a 400 `invalid_request`, as on the
|
|
685
687
|
* token endpoint.
|
|
686
688
|
*
|
|
689
|
+
* Repeated `token`, `token_type_hint`, `client_id` or `client_secret` form
|
|
690
|
+
* parameters are refused before the context is resolved.
|
|
691
|
+
*
|
|
687
692
|
* @see https://datatracker.ietf.org/doc/html/rfc7662
|
|
688
693
|
*/
|
|
689
694
|
handleIntrospectionRequest(request: Request): Promise<Response>;
|
|
@@ -739,6 +744,9 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
|
|
|
739
744
|
* `application/x-www-form-urlencoded`, is a 400 `invalid_request`, as on the
|
|
740
745
|
* token endpoint.
|
|
741
746
|
*
|
|
747
|
+
* Repeated `scope`, `client_id` or `client_secret` form parameters are
|
|
748
|
+
* refused before the context is resolved.
|
|
749
|
+
*
|
|
742
750
|
* @see https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
|
|
743
751
|
* @see https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
|
|
744
752
|
*/
|
|
@@ -348,6 +348,37 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
348
348
|
}
|
|
349
349
|
return grant;
|
|
350
350
|
}
|
|
351
|
+
#assertSingletonParameters(body, parameters) {
|
|
352
|
+
for (const parameter of parameters) {
|
|
353
|
+
if (body.getAll(parameter).length > 1) {
|
|
354
|
+
throw new InvalidRequestError(`${parameter} parameter must not be repeated`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
#assertTokenSingletonParameters(body) {
|
|
359
|
+
this.#assertSingletonParameters(body, ["grant_type"]);
|
|
360
|
+
let parameters;
|
|
361
|
+
switch (body.get("grant_type")) {
|
|
362
|
+
case "authorization_code":
|
|
363
|
+
parameters = ["code", "redirect_uri", "code_verifier"];
|
|
364
|
+
break;
|
|
365
|
+
case "refresh_token":
|
|
366
|
+
parameters = ["refresh_token", "scope"];
|
|
367
|
+
break;
|
|
368
|
+
case "password":
|
|
369
|
+
parameters = ["username", "password", "scope"];
|
|
370
|
+
break;
|
|
371
|
+
case "client_credentials":
|
|
372
|
+
parameters = ["scope"];
|
|
373
|
+
break;
|
|
374
|
+
case DEVICE_AUTHORIZATION_GRANT_TYPE:
|
|
375
|
+
parameters = ["device_code"];
|
|
376
|
+
break;
|
|
377
|
+
default:
|
|
378
|
+
parameters = [];
|
|
379
|
+
}
|
|
380
|
+
this.#assertSingletonParameters(body, parameters);
|
|
381
|
+
}
|
|
351
382
|
async #readBody(request, options = {}) {
|
|
352
383
|
const bytes = await readBoundedBody(request, this.#maxBodyBytes, options)
|
|
353
384
|
.catch((cause) => {
|
|
@@ -374,6 +405,8 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
374
405
|
}).formData().catch(() => {
|
|
375
406
|
throw new InvalidRequestError("body must be application/x-www-form-urlencoded");
|
|
376
407
|
});
|
|
408
|
+
this.#assertSingletonParameters(body, ["client_id", "client_secret"]);
|
|
409
|
+
options.validateBody?.(body);
|
|
377
410
|
const context = await this.authorizationContext(request);
|
|
378
411
|
const grant = options.grantFromBody?.(body);
|
|
379
412
|
const client = grant
|
|
@@ -389,7 +422,8 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
389
422
|
* Supports multiple grant types based on the configured grants.
|
|
390
423
|
*
|
|
391
424
|
* A malformed request — wrong method, a content-type other than
|
|
392
|
-
* `application/x-www-form-urlencoded`,
|
|
425
|
+
* `application/x-www-form-urlencoded`, no `grant_type`, or a repeated
|
|
426
|
+
* recognized singleton form parameter — is a 400
|
|
393
427
|
* `invalid_request`. A `grant_type` with no registered grant is
|
|
394
428
|
* `unsupported_grant_type`, checked before client authentication; a client
|
|
395
429
|
* not registered for the grant is `unauthorized_client`. A body over
|
|
@@ -401,6 +435,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
401
435
|
const { context, body, client, grant } = await this
|
|
402
436
|
.#beginClientAuthenticatedRequest(request, {
|
|
403
437
|
grantFromBody: (body) => this.#grantFromBody(body),
|
|
438
|
+
validateBody: (body) => this.#assertTokenSingletonParameters(body),
|
|
404
439
|
});
|
|
405
440
|
const token = await grant.token(request, client, body);
|
|
406
441
|
return await this.createTokenResponse(token, context, grant);
|
|
@@ -894,7 +929,8 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
894
929
|
* endpoint cannot be used as an oracle for whether a token value is live or
|
|
895
930
|
* whose it is. That covers the token only: a malformed request — wrong
|
|
896
931
|
* method, a content-type other than `application/x-www-form-urlencoded`, or
|
|
897
|
-
* no `token` parameter
|
|
932
|
+
* no `token` parameter, or repeated `token`, `token_type_hint`, `client_id` or
|
|
933
|
+
* `client_secret` — is a 400 `invalid_request`, as on the token
|
|
898
934
|
* endpoint.
|
|
899
935
|
*
|
|
900
936
|
* `token_type_hint` orders the lookup rather than restricting it, so a client
|
|
@@ -905,7 +941,12 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
905
941
|
async handleRevocationRequest(request) {
|
|
906
942
|
try {
|
|
907
943
|
const { context, body, client } = await this
|
|
908
|
-
.#beginClientAuthenticatedRequest(request
|
|
944
|
+
.#beginClientAuthenticatedRequest(request, {
|
|
945
|
+
validateBody: (body) => this.#assertSingletonParameters(body, [
|
|
946
|
+
"token",
|
|
947
|
+
"token_type_hint",
|
|
948
|
+
]),
|
|
949
|
+
});
|
|
909
950
|
const token = body.get("token");
|
|
910
951
|
if (typeof token !== "string") {
|
|
911
952
|
throw new InvalidRequestError("token parameter required");
|
|
@@ -979,12 +1020,20 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
979
1020
|
* `application/x-www-form-urlencoded`, is a 400 `invalid_request`, as on the
|
|
980
1021
|
* token endpoint.
|
|
981
1022
|
*
|
|
1023
|
+
* Repeated `token`, `token_type_hint`, `client_id` or `client_secret` form
|
|
1024
|
+
* parameters are refused before the context is resolved.
|
|
1025
|
+
*
|
|
982
1026
|
* @see https://datatracker.ietf.org/doc/html/rfc7662
|
|
983
1027
|
*/
|
|
984
1028
|
async handleIntrospectionRequest(request) {
|
|
985
1029
|
try {
|
|
986
1030
|
const { context, body, client } = await this
|
|
987
|
-
.#beginClientAuthenticatedRequest(request
|
|
1031
|
+
.#beginClientAuthenticatedRequest(request, {
|
|
1032
|
+
validateBody: (body) => this.#assertSingletonParameters(body, [
|
|
1033
|
+
"token",
|
|
1034
|
+
"token_type_hint",
|
|
1035
|
+
]),
|
|
1036
|
+
});
|
|
988
1037
|
const tokenValue = body.get("token");
|
|
989
1038
|
if (typeof tokenValue !== "string") {
|
|
990
1039
|
throw new InvalidRequestError("token parameter required");
|
|
@@ -1164,6 +1213,9 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1164
1213
|
* `application/x-www-form-urlencoded`, is a 400 `invalid_request`, as on the
|
|
1165
1214
|
* token endpoint.
|
|
1166
1215
|
*
|
|
1216
|
+
* Repeated `scope`, `client_id` or `client_secret` form parameters are
|
|
1217
|
+
* refused before the context is resolved.
|
|
1218
|
+
*
|
|
1167
1219
|
* @see https://datatracker.ietf.org/doc/html/rfc8628#section-3.1
|
|
1168
1220
|
* @see https://datatracker.ietf.org/doc/html/rfc8628#section-3.2
|
|
1169
1221
|
*/
|
|
@@ -1172,6 +1224,7 @@ export class AuthorizationServer extends ResourceServer {
|
|
|
1172
1224
|
const { context, body, client } = await this
|
|
1173
1225
|
.#beginClientAuthenticatedRequest(request, {
|
|
1174
1226
|
requiredGrantType: DEVICE_AUTHORIZATION_GRANT_TYPE,
|
|
1227
|
+
validateBody: (body) => this.#assertSingletonParameters(body, ["scope"]),
|
|
1175
1228
|
});
|
|
1176
1229
|
const grant = this.#deviceAuthorizationGrant();
|
|
1177
1230
|
const scopeText = body.get("scope");
|
package/esm/utils/url.d.ts
CHANGED
|
@@ -13,7 +13,9 @@ import "../_dnt.polyfills.js";
|
|
|
13
13
|
* that some browsers resolve as a host, and any value containing a C0 control
|
|
14
14
|
* character or `DEL` — browsers strip TAB/CR/LF before parsing a URL, so
|
|
15
15
|
* `/<TAB>/evil.com` would otherwise navigate to `evil.com`. Anything else
|
|
16
|
-
* collapses to `fallback` (default `/`).
|
|
16
|
+
* collapses to `fallback` (default `/`), which is returned verbatim. The
|
|
17
|
+
* fallback is trusted caller configuration; do not pass user-controlled input
|
|
18
|
+
* there.
|
|
17
19
|
*
|
|
18
20
|
* Percent-encoded control characters (`/%09/x`) are safe and pass through: a
|
|
19
21
|
* browser does not decode them before parsing, so they stay on the origin.
|
|
@@ -34,17 +36,17 @@ import "../_dnt.polyfills.js";
|
|
|
34
36
|
* ```
|
|
35
37
|
*/
|
|
36
38
|
export declare function safeReturnTo(value: string | null | undefined, fallback?: string): string;
|
|
37
|
-
/**
|
|
39
|
+
/** Trusted application configuration for {@link loginContinuation}. */
|
|
38
40
|
export interface LoginContinuationOptions {
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
41
|
-
* it, the flow is **resumed** (the in-flight authorize request already carries
|
|
42
|
+
* Trusted authorization endpoint (absolute URL or path). When `returnTo` points
|
|
43
|
+
* at it, the flow is **resumed** (the in-flight authorize request already carries
|
|
42
44
|
* its `state`). Omit it and every `returnTo` starts a fresh login.
|
|
43
45
|
*/
|
|
44
46
|
authorizeEndpoint?: string;
|
|
45
|
-
/**
|
|
47
|
+
/** Trusted login path for a fresh login. Defaults to `"/auth/login"`. */
|
|
46
48
|
loginPath?: string;
|
|
47
|
-
/**
|
|
49
|
+
/** Trusted fallback for missing/unsafe `returnTo`. Defaults to `"/"`. */
|
|
48
50
|
defaultReturnTo?: string;
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
@@ -61,6 +63,7 @@ export interface LoginContinuationOptions {
|
|
|
61
63
|
*
|
|
62
64
|
* Pure and client-safe — the same logic behind `HonoBff.loginContinuation`
|
|
63
65
|
* (server) and `BffClient.loginContinuation` (browser). `returnTo` is
|
|
64
|
-
* open-redirect-guarded via {@link safeReturnTo}.
|
|
66
|
+
* open-redirect-guarded via {@link safeReturnTo}. The fallback, authorization
|
|
67
|
+
* endpoint and login path are trusted application configuration.
|
|
65
68
|
*/
|
|
66
69
|
export declare function loginContinuation(returnTo: string | null | undefined, options?: LoginContinuationOptions): string;
|
package/esm/utils/url.js
CHANGED
|
@@ -22,7 +22,9 @@ function hasControlCharacter(value) {
|
|
|
22
22
|
* that some browsers resolve as a host, and any value containing a C0 control
|
|
23
23
|
* character or `DEL` — browsers strip TAB/CR/LF before parsing a URL, so
|
|
24
24
|
* `/<TAB>/evil.com` would otherwise navigate to `evil.com`. Anything else
|
|
25
|
-
* collapses to `fallback` (default `/`).
|
|
25
|
+
* collapses to `fallback` (default `/`), which is returned verbatim. The
|
|
26
|
+
* fallback is trusted caller configuration; do not pass user-controlled input
|
|
27
|
+
* there.
|
|
26
28
|
*
|
|
27
29
|
* Percent-encoded control characters (`/%09/x`) are safe and pass through: a
|
|
28
30
|
* browser does not decode them before parsing, so they stay on the origin.
|
|
@@ -74,7 +76,8 @@ export function safeReturnTo(value, fallback = "/") {
|
|
|
74
76
|
*
|
|
75
77
|
* Pure and client-safe — the same logic behind `HonoBff.loginContinuation`
|
|
76
78
|
* (server) and `BffClient.loginContinuation` (browser). `returnTo` is
|
|
77
|
-
* open-redirect-guarded via {@link safeReturnTo}.
|
|
79
|
+
* open-redirect-guarded via {@link safeReturnTo}. The fallback, authorization
|
|
80
|
+
* endpoint and login path are trusted application configuration.
|
|
78
81
|
*/
|
|
79
82
|
export function loginContinuation(returnTo, options = {}) {
|
|
80
83
|
const safe = safeReturnTo(returnTo, options.defaultReturnTo ?? "/");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@udibo/oauth2",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
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
|
"repository": {
|
|
6
6
|
"type": "git",
|