@udibo/oauth2 0.28.0 → 0.29.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.
@@ -678,6 +678,8 @@ export declare class AuthorizationServer<Client extends ClientInterface, User, S
678
678
  * authenticate through the same implementation, so a request that fails here
679
679
  * fails identically at the token endpoint.
680
680
  *
681
+ * @throws {InvalidRequestError} If the request uses more than one client
682
+ * authentication method (RFC 6749 Section 2.3)
681
683
  * @throws {InvalidClientError} If client authentication fails
682
684
  */
683
685
  authenticateClient(request: Request, body: FormData, services: AuthorizationServerServices<Client, User, S>): Promise<Client>;
@@ -970,6 +970,8 @@ export class AuthorizationServer extends ResourceServer {
970
970
  * authenticate through the same implementation, so a request that fails here
971
971
  * fails identically at the token endpoint.
972
972
  *
973
+ * @throws {InvalidRequestError} If the request uses more than one client
974
+ * authentication method (RFC 6749 Section 2.3)
973
975
  * @throws {InvalidClientError} If client authentication fails
974
976
  */
975
977
  async authenticateClient(request, body, services) {
@@ -9,13 +9,19 @@
9
9
  import type { ClientCredentials, ClientInterface } from "../models/client.ts";
10
10
  import type { ClientServiceInterface } from "./services/client.ts";
11
11
  /**
12
- * Reads the credentials a request presents: HTTP Basic first (RFC 6749 Section
13
- * 2.3.1), then `client_id`/`client_secret` in `body`, the form body the
14
- * endpoint already parsed. An `Authorization` header that is not usable Basic
15
- * falls through to the body, so a request that also carries body credentials
16
- * authenticates by the method it did present.
12
+ * Reads the credentials a request presents by the one client-authentication
13
+ * method it uses: HTTP Basic (RFC 6749 Section 2.3.1), or
14
+ * `client_id`/`client_secret` in `body`, the form body the endpoint already
15
+ * parsed. A `client_id` alone identifies a public client. An `Authorization`
16
+ * header with a scheme other than Basic is not client authentication and is
17
+ * ignored.
17
18
  *
18
- * @throws {InvalidClientError} If the request presents no usable credentials.
19
+ * @throws {InvalidRequestError} If the request uses more than one method —
20
+ * Basic plus a body `client_secret` or a `client_assertion`, or a body
21
+ * `client_secret` plus a `client_assertion` — or sends Basic credentials
22
+ * with a body `client_id` naming a different client.
23
+ * @throws {InvalidClientError} If the request presents no usable credentials,
24
+ * or its Basic header is malformed.
19
25
  */
20
26
  export declare function extractClientCredentials(request: Request, body: FormData): ClientCredentials;
21
27
  /**
@@ -6,34 +6,54 @@
6
6
  * @see https://datatracker.ietf.org/doc/html/rfc6749#section-2.3
7
7
  * @module
8
8
  */
9
- import { InvalidClientError } from "../errors.js";
10
- import { parseBasicAuth, tryParseBasicAuth } from "../utils/basic-auth.js";
9
+ import { InvalidClientError, InvalidRequestError } from "../errors.js";
10
+ import { parseBasicAuth } from "../utils/basic-auth.js";
11
+ const BASIC_SCHEME = /(?:^|,)[ \t]*basic(?:[ \t]|$)/i;
11
12
  /**
12
- * Reads the credentials a request presents: HTTP Basic first (RFC 6749 Section
13
- * 2.3.1), then `client_id`/`client_secret` in `body`, the form body the
14
- * endpoint already parsed. An `Authorization` header that is not usable Basic
15
- * falls through to the body, so a request that also carries body credentials
16
- * authenticates by the method it did present.
13
+ * Reads the credentials a request presents by the one client-authentication
14
+ * method it uses: HTTP Basic (RFC 6749 Section 2.3.1), or
15
+ * `client_id`/`client_secret` in `body`, the form body the endpoint already
16
+ * parsed. A `client_id` alone identifies a public client. An `Authorization`
17
+ * header with a scheme other than Basic is not client authentication and is
18
+ * ignored.
17
19
  *
18
- * @throws {InvalidClientError} If the request presents no usable credentials.
20
+ * @throws {InvalidRequestError} If the request uses more than one method —
21
+ * Basic plus a body `client_secret` or a `client_assertion`, or a body
22
+ * `client_secret` plus a `client_assertion` — or sends Basic credentials
23
+ * with a body `client_id` naming a different client.
24
+ * @throws {InvalidClientError} If the request presents no usable credentials,
25
+ * or its Basic header is malformed.
19
26
  */
20
27
  export function extractClientCredentials(request, body) {
21
28
  const authorization = request.headers.get("authorization");
22
- const basicAuth = tryParseBasicAuth(authorization);
23
- if (basicAuth) {
29
+ const usesBasic = !!authorization && BASIC_SCHEME.test(authorization);
30
+ const clientId = body.get("client_id");
31
+ const clientSecret = body.get("client_secret");
32
+ const methods = [
33
+ usesBasic,
34
+ clientSecret !== null,
35
+ body.has("client_assertion") || body.has("client_assertion_type"),
36
+ ].filter(Boolean).length;
37
+ if (methods > 1) {
38
+ throw new InvalidRequestError("multiple client authentication methods");
39
+ }
40
+ if (usesBasic) {
41
+ const basicAuth = parseBasicAuth(authorization);
42
+ if (clientId !== null && clientId !== basicAuth.name) {
43
+ throw new InvalidRequestError("client_id does not match the Basic credentials");
44
+ }
24
45
  return { clientId: basicAuth.name, clientSecret: basicAuth.pass };
25
46
  }
26
- const clientId = body.get("client_id");
27
47
  if (typeof clientId === "string") {
28
48
  const credentials = { clientId };
29
- const clientSecret = body.get("client_secret");
30
49
  if (typeof clientSecret === "string") {
31
50
  credentials.clientSecret = clientSecret;
32
51
  }
33
52
  return credentials;
34
53
  }
35
- if (authorization)
36
- throw parseBasicAuth(authorization);
54
+ if (authorization) {
55
+ throw new InvalidClientError("unsupported authorization header");
56
+ }
37
57
  throw new InvalidClientError("client authentication required");
38
58
  }
39
59
  /**
@@ -116,11 +116,13 @@ export declare abstract class AbstractGrant<Client extends ClientInterface, User
116
116
  */
117
117
  acceptedScope(client: Client, user: User | undefined, scope: Scope | null | undefined, tokenService: TokenServiceInterface<Client, User, Scope>): Promise<Scope | null | undefined>;
118
118
  /**
119
- * Extracts client credentials from a request: HTTP Basic first, then the
120
- * `client_id`/`client_secret` of the already-parsed `body`. Override to add
121
- * grant-specific credential rules (as the authorization-code grant does for
122
- * PKCE).
119
+ * Extracts client credentials from a request by the one method it uses: HTTP
120
+ * Basic, or the `client_id`/`client_secret` of the already-parsed `body`.
121
+ * Override to add grant-specific credential rules (as the authorization-code
122
+ * grant does for PKCE).
123
123
  *
124
+ * @throws {InvalidRequestError} If the request uses more than one client
125
+ * authentication method.
124
126
  * @throws {InvalidClientError} If the request presents no usable credentials.
125
127
  */
126
128
  protected getClientCredentials(request: Request, body: FormData): ClientCredentials;
@@ -70,11 +70,13 @@ export class AbstractGrant {
70
70
  return acceptedScope ?? scope;
71
71
  }
72
72
  /**
73
- * Extracts client credentials from a request: HTTP Basic first, then the
74
- * `client_id`/`client_secret` of the already-parsed `body`. Override to add
75
- * grant-specific credential rules (as the authorization-code grant does for
76
- * PKCE).
73
+ * Extracts client credentials from a request by the one method it uses: HTTP
74
+ * Basic, or the `client_id`/`client_secret` of the already-parsed `body`.
75
+ * Override to add grant-specific credential rules (as the authorization-code
76
+ * grant does for PKCE).
77
77
  *
78
+ * @throws {InvalidRequestError} If the request uses more than one client
79
+ * authentication method.
78
80
  * @throws {InvalidClientError} If the request presents no usable credentials.
79
81
  */
80
82
  getClientCredentials(request, body) {
@@ -113,6 +113,9 @@ export declare function validateCodeChallenge(challenge: string, method?: string
113
113
  * own, callable properties of `methods` resolve here; every other name is
114
114
  * `undefined`, including names inherited from a prototype.
115
115
  *
116
+ * An omitted method means `S256`, deliberately not RFC 7636 §4.3's `plain`,
117
+ * following OAuth 2.1.
118
+ *
116
119
  * @param methods The challenge methods the server allows
117
120
  * @param method The client-supplied method name; defaults to `S256`
118
121
  * @returns The transform for that method, or `undefined` when it is not allowed
@@ -127,6 +127,9 @@ export function validateCodeChallenge(challenge, method) {
127
127
  * own, callable properties of `methods` resolve here; every other name is
128
128
  * `undefined`, including names inherited from a prototype.
129
129
  *
130
+ * An omitted method means `S256`, deliberately not RFC 7636 §4.3's `plain`,
131
+ * following OAuth 2.1.
132
+ *
130
133
  * @param methods The challenge methods the server allows
131
134
  * @param method The client-supplied method name; defaults to `S256`
132
135
  * @returns The transform for that method, or `undefined` when it is not allowed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@udibo/oauth2",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",