@udibo/oauth2 0.28.0 → 0.30.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/server/authorization-server.d.ts +2 -0
- package/dist/server/authorization-server.js +2 -0
- package/dist/server/client-authentication.d.ts +12 -6
- package/dist/server/client-authentication.js +34 -14
- package/dist/server/grants/grant.d.ts +6 -4
- package/dist/server/grants/grant.js +6 -4
- package/dist/server/jwks-token-reader.d.ts +29 -12
- package/dist/server/jwks-token-reader.js +16 -7
- package/dist/utils/pkce.d.ts +3 -0
- package/dist/utils/pkce.js +3 -0
- package/package.json +1 -1
|
@@ -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
|
|
13
|
-
* 2.3.1),
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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 {
|
|
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
|
|
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
|
|
13
|
-
* 2.3.1),
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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 {
|
|
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
|
|
23
|
-
|
|
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
|
|
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
|
|
120
|
-
* `client_id`/`client_secret` of the already-parsed `body`.
|
|
121
|
-
* grant-specific credential rules (as the authorization-code
|
|
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
|
|
74
|
-
* `client_id`/`client_secret` of the already-parsed `body`.
|
|
75
|
-
* grant-specific credential rules (as the authorization-code
|
|
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) {
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Token reader that validates JWT access tokens locally against the
|
|
3
3
|
* issuer's published JWKS, with no per-request network hop.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* It validates RFC 9068 (`at+jwt`) access tokens: the authorization server
|
|
6
|
+
* signs access tokens and publishes its public keys at a `jwks_uri`; the
|
|
7
|
+
* resource server fetches those keys once, caches them, and verifies every
|
|
8
|
+
* subsequent token in-process, including the `typ` header and every claim
|
|
9
|
+
* RFC 9068 §2.2 makes REQUIRED. Signature verification uses Web Crypto
|
|
9
10
|
* only — the package takes no JOSE dependency.
|
|
10
11
|
*
|
|
11
12
|
* @see https://datatracker.ietf.org/doc/html/rfc9068
|
|
@@ -27,19 +28,25 @@ import type { TokenReaderInterface } from "./services/token.ts";
|
|
|
27
28
|
export interface JwtAccessTokenClaims {
|
|
28
29
|
/** Issuer that signed the token. Verified against the configured issuer. */
|
|
29
30
|
iss: string;
|
|
30
|
-
/** Subject — the end user, or the client itself for client-credentials tokens. */
|
|
31
|
-
sub
|
|
31
|
+
/** Subject — the end user, or the client itself for client-credentials tokens. Required. */
|
|
32
|
+
sub: string;
|
|
32
33
|
/** Audience(s) the token is intended for. Verified against the configured audience. */
|
|
33
34
|
aud: string | string[];
|
|
34
35
|
/** Expiration time, seconds since the epoch. */
|
|
35
36
|
exp: number;
|
|
36
|
-
/** Issued-at time, seconds since the epoch. */
|
|
37
|
-
iat
|
|
37
|
+
/** Issued-at time, seconds since the epoch. Required. */
|
|
38
|
+
iat: number;
|
|
38
39
|
/** Not-before time, seconds since the epoch. */
|
|
39
40
|
nbf?: number;
|
|
40
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Unique token identifier. Required unless
|
|
43
|
+
* {@linkcode JwksTokenReaderOptions.tokenIdClaim} names another claim.
|
|
44
|
+
*/
|
|
41
45
|
jti?: string;
|
|
42
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Client the token was issued to. Required unless
|
|
48
|
+
* {@linkcode JwksTokenReaderOptions.clientIdClaim} names another claim.
|
|
49
|
+
*/
|
|
43
50
|
client_id?: string;
|
|
44
51
|
/** Space-delimited granted scopes. */
|
|
45
52
|
scope?: string;
|
|
@@ -105,9 +112,18 @@ export interface JwksTokenReaderOptions<Client extends ClientInterface, User, S
|
|
|
105
112
|
* §2.2 makes `client_id` REQUIRED, and its absence is what tells an
|
|
106
113
|
* `id_token` apart from an access token). Defaults to `"client_id"`; point
|
|
107
114
|
* it at the vendor claim for issuers that deviate — Okta's `cid`, Azure
|
|
108
|
-
* AD v1's `appid`.
|
|
115
|
+
* AD v1's `appid`. The requirement cannot be turned off, but pointing this
|
|
116
|
+
* at a claim every token carries defeats it.
|
|
109
117
|
*/
|
|
110
118
|
clientIdClaim?: string;
|
|
119
|
+
/**
|
|
120
|
+
* Claim carrying the unique token identifier, required on every accepted
|
|
121
|
+
* token (RFC 9068 §2.2 makes `jti` REQUIRED). Defaults to `"jti"`; point it
|
|
122
|
+
* at the vendor claim for issuers that deviate — Microsoft Entra ID's
|
|
123
|
+
* `uti`. The requirement cannot be turned off, but pointing this at a claim
|
|
124
|
+
* every token carries defeats it.
|
|
125
|
+
*/
|
|
126
|
+
tokenIdClaim?: string;
|
|
111
127
|
/**
|
|
112
128
|
* Leeway in seconds applied to `exp` and `nbf` to absorb clock drift
|
|
113
129
|
* between the issuer and this server. Defaults to 30. Must be finite and
|
|
@@ -239,7 +255,8 @@ export declare class JwksTokenReader<Client extends ClientInterface, User, S ext
|
|
|
239
255
|
#private;
|
|
240
256
|
/**
|
|
241
257
|
* Defaults: every Web Crypto asymmetric algorithm accepted, `at+jwt`
|
|
242
|
-
* required as `typ`, `client_id` as the required client-id claim,
|
|
258
|
+
* required as `typ`, `client_id` as the required client-id claim, `jti` as
|
|
259
|
+
* the required token-identifier claim, 30
|
|
243
260
|
* seconds of clock skew, a 10-minute cache with a 30-second minimum
|
|
244
261
|
* between fetches, a 5-second request deadline, a 256 KiB response cap,
|
|
245
262
|
* {@linkcode BasicScope}, and the global `fetch`.
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Token reader that validates JWT access tokens locally against the
|
|
3
3
|
* issuer's published JWKS, with no per-request network hop.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* It validates RFC 9068 (`at+jwt`) access tokens: the authorization server
|
|
6
|
+
* signs access tokens and publishes its public keys at a `jwks_uri`; the
|
|
7
|
+
* resource server fetches those keys once, caches them, and verifies every
|
|
8
|
+
* subsequent token in-process, including the `typ` header and every claim
|
|
9
|
+
* RFC 9068 §2.2 makes REQUIRED. Signature verification uses Web Crypto
|
|
9
10
|
* only — the package takes no JOSE dependency.
|
|
10
11
|
*
|
|
11
12
|
* @see https://datatracker.ietf.org/doc/html/rfc9068
|
|
@@ -208,6 +209,7 @@ export class JwksTokenReader {
|
|
|
208
209
|
#algorithms;
|
|
209
210
|
#types;
|
|
210
211
|
#clientIdClaim;
|
|
212
|
+
#tokenIdClaim;
|
|
211
213
|
#clockSkewSeconds;
|
|
212
214
|
#cacheMaxAgeMs;
|
|
213
215
|
#minFetchIntervalMs;
|
|
@@ -225,7 +227,8 @@ export class JwksTokenReader {
|
|
|
225
227
|
#inFlight;
|
|
226
228
|
/**
|
|
227
229
|
* Defaults: every Web Crypto asymmetric algorithm accepted, `at+jwt`
|
|
228
|
-
* required as `typ`, `client_id` as the required client-id claim,
|
|
230
|
+
* required as `typ`, `client_id` as the required client-id claim, `jti` as
|
|
231
|
+
* the required token-identifier claim, 30
|
|
229
232
|
* seconds of clock skew, a 10-minute cache with a 30-second minimum
|
|
230
233
|
* between fetches, a 5-second request deadline, a 256 KiB response cap,
|
|
231
234
|
* {@linkcode BasicScope}, and the global `fetch`.
|
|
@@ -254,6 +257,7 @@ export class JwksTokenReader {
|
|
|
254
257
|
}
|
|
255
258
|
this.#types = (options.types ?? ["at+jwt"]).map(normalizeType);
|
|
256
259
|
this.#clientIdClaim = options.clientIdClaim ?? "client_id";
|
|
260
|
+
this.#tokenIdClaim = options.tokenIdClaim ?? "jti";
|
|
257
261
|
this.#clockSkewSeconds = options.clockSkewSeconds ?? 30;
|
|
258
262
|
assertClockSkewSeconds(this.#clockSkewSeconds);
|
|
259
263
|
this.#cacheMaxAgeMs = options.cacheMaxAgeMs ?? 600_000;
|
|
@@ -336,9 +340,14 @@ export class JwksTokenReader {
|
|
|
336
340
|
#validClaims(claims) {
|
|
337
341
|
if (claims.iss !== this.#issuer)
|
|
338
342
|
return false;
|
|
339
|
-
const
|
|
340
|
-
|
|
343
|
+
for (const claim of [this.#clientIdClaim, this.#tokenIdClaim, "sub"]) {
|
|
344
|
+
const value = claims[claim];
|
|
345
|
+
if (typeof value !== "string" || !value)
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
if (typeof claims.iat !== "number" || !Number.isFinite(claims.iat)) {
|
|
341
349
|
return false;
|
|
350
|
+
}
|
|
342
351
|
for (const claim of ID_TOKEN_ONLY_CLAIMS) {
|
|
343
352
|
if (claims[claim] !== undefined)
|
|
344
353
|
return false;
|
package/dist/utils/pkce.d.ts
CHANGED
|
@@ -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
|
package/dist/utils/pkce.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.30.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",
|