@udibo/oauth2 0.4.1 → 0.5.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/README.md +3 -3
- package/SECURITY.md +0 -4
- package/esm/adapters/hono/_common.d.ts +10 -13
- package/esm/adapters/hono/_common.js +11 -20
- package/esm/adapters/hono/authorization-server.d.ts +16 -10
- package/esm/adapters/hono/authorization-server.js +13 -8
- package/esm/adapters/hono/bff/auth-request-store.d.ts +4 -0
- package/esm/adapters/hono/bff/auth-request-store.js +4 -0
- package/esm/adapters/hono/bff/bff.d.ts +29 -18
- package/esm/adapters/hono/bff/bff.js +15 -11
- package/esm/adapters/hono/bff/proxy.js +7 -55
- package/esm/adapters/hono/bff/session-store.d.ts +9 -9
- package/esm/adapters/hono/bff/session-store.js +3 -2
- package/esm/adapters/hono/bff/testing.d.ts +14 -3
- package/esm/adapters/hono/bff/testing.js +14 -3
- package/esm/adapters/hono/identity.d.ts +5 -3
- package/esm/adapters/hono/identity.js +1 -17
- package/esm/adapters/hono/resource-server.d.ts +3 -2
- package/esm/adapters/hono/resource-server.js +3 -2
- package/esm/client/_http.d.ts +4 -7
- package/esm/client/_http.js +4 -37
- package/esm/client/base.d.ts +3 -6
- package/esm/client/bff-client.d.ts +7 -6
- package/esm/client/bff-client.js +7 -6
- package/esm/client/browser-storage.d.ts +33 -34
- package/esm/client/browser-storage.js +47 -32
- package/esm/client/check.d.ts +10 -3
- package/esm/client/check.js +28 -8
- package/esm/client/direct-client.d.ts +49 -29
- package/esm/client/direct-client.js +59 -59
- package/esm/client/discovery-cache.d.ts +12 -5
- package/esm/client/discovery-cache.js +11 -4
- package/esm/client/events.d.ts +11 -3
- package/esm/client/events.js +1 -1
- package/esm/client/mod.d.ts +6 -4
- package/esm/client/mod.js +5 -3
- package/esm/client/storage.d.ts +53 -23
- package/esm/client/storage.js +43 -15
- package/esm/errors.d.ts +15 -12
- package/esm/errors.js +12 -8
- package/esm/identity/delivery.d.ts +7 -7
- package/esm/identity/delivery.js +1 -2
- package/esm/identity/errors.d.ts +10 -4
- package/esm/identity/errors.js +4 -1
- package/esm/identity/events.d.ts +21 -5
- package/esm/identity/events.js +4 -2
- package/esm/identity/external/_shared.d.ts +5 -7
- package/esm/identity/external/_shared.js +5 -7
- package/esm/identity/external/_token-exchange.d.ts +3 -6
- package/esm/identity/external/_token-exchange.js +2 -5
- package/esm/identity/external/apple-client-secret.d.ts +13 -8
- package/esm/identity/external/apple-client-secret.js +9 -6
- package/esm/identity/external/apple.d.ts +17 -10
- package/esm/identity/external/apple.js +12 -9
- package/esm/identity/external/discord.d.ts +5 -1
- package/esm/identity/external/errors.d.ts +6 -2
- package/esm/identity/external/errors.js +6 -2
- package/esm/identity/external/flow.d.ts +9 -6
- package/esm/identity/external/flow.js +6 -5
- package/esm/identity/external/github.d.ts +7 -3
- package/esm/identity/external/github.js +2 -2
- package/esm/identity/external/google.d.ts +5 -1
- package/esm/identity/external/oauth2.d.ts +13 -2
- package/esm/identity/external/oauth2.js +3 -3
- package/esm/identity/external/oidc.d.ts +21 -10
- package/esm/identity/external/oidc.js +11 -6
- package/esm/identity/external/provider.d.ts +4 -2
- package/esm/identity/identifier.d.ts +6 -5
- package/esm/identity/identifier.js +6 -5
- package/esm/identity/lockout.d.ts +4 -3
- package/esm/identity/lockout.js +4 -3
- package/esm/identity/mfa/mod.d.ts +2 -1
- package/esm/identity/mfa/mod.js +2 -1
- package/esm/identity/mfa/recovery-codes.d.ts +2 -1
- package/esm/identity/mfa/service.d.ts +29 -11
- package/esm/identity/mfa/service.js +35 -11
- package/esm/identity/mfa/totp.d.ts +4 -2
- package/esm/identity/mfa/totp.js +4 -2
- package/esm/identity/migration.d.ts +8 -7
- package/esm/identity/migration.js +9 -12
- package/esm/identity/mod.d.ts +2 -2
- package/esm/identity/mod.js +2 -2
- package/esm/identity/otp.d.ts +23 -16
- package/esm/identity/otp.js +13 -12
- package/esm/identity/password-policy.d.ts +3 -3
- package/esm/identity/password-policy.js +3 -3
- package/esm/identity/password.d.ts +23 -20
- package/esm/identity/password.js +15 -16
- package/esm/identity/rate-limit.d.ts +1 -7
- package/esm/identity/rate-limit.js +1 -2
- package/esm/identity/service.d.ts +76 -44
- package/esm/identity/service.js +86 -60
- package/esm/identity/session.d.ts +5 -4
- package/esm/identity/session.js +5 -4
- package/esm/identity/token-flow.d.ts +8 -8
- package/esm/identity/token-flow.js +5 -6
- package/esm/models/client.d.ts +7 -4
- package/esm/models/token.d.ts +2 -3
- package/esm/react/callback.d.ts +3 -3
- package/esm/react/callback.js +3 -3
- package/esm/react/components/class-names.d.ts +3 -6
- package/esm/react/components/class-names.js +3 -6
- package/esm/react/components/internal.d.ts +7 -17
- package/esm/react/components/internal.js +7 -17
- package/esm/react/components/mod.d.ts +7 -5
- package/esm/react/components/mod.js +7 -5
- package/esm/react/components/types.d.ts +8 -6
- package/esm/react/context.d.ts +3 -3
- package/esm/react/provider.d.ts +3 -2
- package/esm/react/provider.js +0 -13
- package/esm/react/require-auth.d.ts +0 -3
- package/esm/react/require-auth.js +0 -3
- package/esm/react/testing.d.ts +3 -2
- package/esm/react/testing.js +3 -3
- package/esm/react/use-oauth2.d.ts +1 -1
- package/esm/server/authorization-server.d.ts +81 -54
- package/esm/server/authorization-server.js +76 -100
- package/esm/server/grants/authorization-code.d.ts +7 -3
- package/esm/server/grants/authorization-code.js +6 -2
- package/esm/server/grants/device-authorization.d.ts +1 -1
- package/esm/server/grants/grant.d.ts +15 -8
- package/esm/server/grants/grant.js +26 -18
- package/esm/server/grants/password.d.ts +1 -1
- package/esm/server/grants/refresh-token.d.ts +6 -3
- package/esm/server/grants/refresh-token.js +4 -3
- package/esm/server/introspection-token-reader.d.ts +16 -7
- package/esm/server/introspection-token-reader.js +9 -3
- package/esm/server/jwks-token-reader.d.ts +3 -3
- package/esm/server/jwks-token-reader.js +3 -3
- package/esm/server/redirect-uri.d.ts +6 -9
- package/esm/server/redirect-uri.js +7 -34
- package/esm/server/resource-server.d.ts +5 -3
- package/esm/server/resource-server.js +5 -3
- package/esm/server/services/client.d.ts +5 -1
- package/esm/server/services/device-authorization.d.ts +6 -6
- package/esm/server/services/token.d.ts +15 -8
- package/esm/server/services/token.js +1 -1
- package/esm/server/signing-keys.d.ts +24 -9
- package/esm/server/signing-keys.js +20 -9
- package/esm/testing/contract/mod.d.ts +3 -3
- package/esm/testing/contract/mod.js +3 -3
- package/esm/testing/mod.d.ts +4 -2
- package/esm/testing/mod.js +4 -2
- package/esm/testing/services.d.ts +1 -1
- package/esm/testing/services.js +1 -1
- package/esm/testing/tenant.d.ts +13 -4
- package/esm/utils/_buffer.d.ts +3 -2
- package/esm/utils/_buffer.js +3 -2
- package/esm/utils/_timing-safe.d.ts +8 -6
- package/esm/utils/_timing-safe.js +8 -6
- package/esm/utils/basic-auth.d.ts +5 -12
- package/esm/utils/basic-auth.js +5 -12
- package/esm/utils/crypto.d.ts +2 -1
- package/esm/utils/crypto.js +2 -1
- package/esm/utils/text.d.ts +2 -9
- package/esm/utils/text.js +2 -9
- package/esm/utils/url.d.ts +2 -2
- package/esm/utils/url.js +0 -2
- package/package.json +1 -1
|
@@ -40,8 +40,9 @@ export class RefreshTokenGrant extends AbstractGrant {
|
|
|
40
40
|
* authorization.
|
|
41
41
|
*
|
|
42
42
|
* @throws {InvalidRequestError} If `refresh_token` is missing.
|
|
43
|
-
* @throws {InvalidGrantError} If the refresh token is unknown or expired,
|
|
44
|
-
*
|
|
43
|
+
* @throws {InvalidGrantError} If the refresh token is unknown or expired, its
|
|
44
|
+
* family has outlived the absolute cap, or a concurrent exchange already
|
|
45
|
+
* claimed it.
|
|
45
46
|
* @throws {InvalidClientError} If the token was issued to another client.
|
|
46
47
|
* @throws {InvalidScopeError} If the requested scope exceeds the original grant.
|
|
47
48
|
*/
|
|
@@ -98,7 +99,7 @@ export class RefreshTokenGrant extends AbstractGrant {
|
|
|
98
99
|
if (familyExpiresAt && familyExpiresAt.getTime() <= Date.now()) {
|
|
99
100
|
throw new InvalidGrantError("refresh_token family expired");
|
|
100
101
|
}
|
|
101
|
-
const nextToken = await this.generateToken(client, user, nextScope, tokenService, currentToken.authenticationContext);
|
|
102
|
+
const nextToken = await this.generateToken(client, user, nextScope, tokenService, currentToken.authenticationContext, familyExpiresAt);
|
|
102
103
|
const nextRefreshToken = "refreshToken" in nextToken
|
|
103
104
|
? nextToken
|
|
104
105
|
: {
|
|
@@ -46,10 +46,13 @@ export interface IntrospectionTokenReaderOptions<Client extends ClientInterface,
|
|
|
46
46
|
* `undefined` when the token doesn't represent a user. Omit entirely if
|
|
47
47
|
* your app never reads user info off the token.
|
|
48
48
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
49
|
+
* This package's authorization server sets `sub` from its `subjectOf`
|
|
50
|
+
* option (the user's `id` by default — the same value its id_token and
|
|
51
|
+
* UserInfo carry) and omits it for a token with no user, whatever its
|
|
52
|
+
* `introspectionClaims` hook returns, so `data.sub` is present only when
|
|
53
|
+
* there is a resource owner and `data.sub ? {…} : undefined` is the whole
|
|
54
|
+
* discriminator. Another authorization server may set `sub` to the client
|
|
55
|
+
* id for a machine token; check how yours answers.
|
|
53
56
|
*
|
|
54
57
|
* May be async — return a promise to enrich from a DB lookup or the
|
|
55
58
|
* authorization server's OIDC UserInfo endpoint, using `data.sub` as
|
|
@@ -81,6 +84,11 @@ export interface IntrospectionTokenReaderOptions<Client extends ClientInterface,
|
|
|
81
84
|
* types via the {@linkcode IntrospectionTokenReaderOptions.getClient} and
|
|
82
85
|
* {@linkcode IntrospectionTokenReaderOptions.getUser} mappers.
|
|
83
86
|
*
|
|
87
|
+
* The reader does not check `aud` or `iss`: any token the endpoint reports
|
|
88
|
+
* active with a bearer `token_type` is accepted. When one authorization server
|
|
89
|
+
* issues tokens for several APIs, check the audience yourself (it is on
|
|
90
|
+
* {@linkcode Token.claims}) or have the endpoint scope its answers.
|
|
91
|
+
*
|
|
84
92
|
* @example
|
|
85
93
|
* ```ts
|
|
86
94
|
* import {
|
|
@@ -137,9 +145,10 @@ export declare class IntrospectionTokenReader<Client extends ClientInterface, Us
|
|
|
137
145
|
* returns a 5xx — a down authorization server must be distinguishable from
|
|
138
146
|
* a genuinely invalid token (which is `undefined` → `invalid_token`), not
|
|
139
147
|
* collapsed into it.
|
|
140
|
-
* @throws {ServerError} If the endpoint returns
|
|
141
|
-
* introspection credentials are rejected)
|
|
142
|
-
* verdict on the caller's
|
|
148
|
+
* @throws {ServerError} If the endpoint returns any other non-2xx status
|
|
149
|
+
* (e.g. the reader's own introspection credentials are rejected) or a body
|
|
150
|
+
* that is not JSON — a misconfiguration, not a verdict on the caller's
|
|
151
|
+
* token.
|
|
143
152
|
*
|
|
144
153
|
* An active response is only accepted when its `token_type` names a bearer
|
|
145
154
|
* token. RFC 7662 answers for refresh tokens too, and a refresh token's
|
|
@@ -22,6 +22,11 @@ import { encodeBasicAuth } from "../utils/basic-auth.js";
|
|
|
22
22
|
* types via the {@linkcode IntrospectionTokenReaderOptions.getClient} and
|
|
23
23
|
* {@linkcode IntrospectionTokenReaderOptions.getUser} mappers.
|
|
24
24
|
*
|
|
25
|
+
* The reader does not check `aud` or `iss`: any token the endpoint reports
|
|
26
|
+
* active with a bearer `token_type` is accepted. When one authorization server
|
|
27
|
+
* issues tokens for several APIs, check the audience yourself (it is on
|
|
28
|
+
* {@linkcode Token.claims}) or have the endpoint scope its answers.
|
|
29
|
+
*
|
|
25
30
|
* @example
|
|
26
31
|
* ```ts
|
|
27
32
|
* import {
|
|
@@ -87,9 +92,10 @@ export class IntrospectionTokenReader {
|
|
|
87
92
|
* returns a 5xx — a down authorization server must be distinguishable from
|
|
88
93
|
* a genuinely invalid token (which is `undefined` → `invalid_token`), not
|
|
89
94
|
* collapsed into it.
|
|
90
|
-
* @throws {ServerError} If the endpoint returns
|
|
91
|
-
* introspection credentials are rejected)
|
|
92
|
-
* verdict on the caller's
|
|
95
|
+
* @throws {ServerError} If the endpoint returns any other non-2xx status
|
|
96
|
+
* (e.g. the reader's own introspection credentials are rejected) or a body
|
|
97
|
+
* that is not JSON — a misconfiguration, not a verdict on the caller's
|
|
98
|
+
* token.
|
|
93
99
|
*
|
|
94
100
|
* An active response is only accepted when its `token_type` names a bearer
|
|
95
101
|
* token. RFC 7662 answers for refresh tokens too, and a refresh token's
|
|
@@ -255,9 +255,9 @@ export declare class JwksTokenReader<Client extends ClientInterface, User, S ext
|
|
|
255
255
|
* available — a down issuer must be distinguishable from a genuinely
|
|
256
256
|
* invalid token, not collapsed into it. Once keys are cached, a failed
|
|
257
257
|
* refresh is absorbed and the cached keys keep serving.
|
|
258
|
-
* @throws {ServerError} If the JWKS endpoint returns
|
|
259
|
-
* is not a JWKS, or discovery reports no `jwks_uri`
|
|
260
|
-
* misconfiguration, not a verdict on the caller's token.
|
|
258
|
+
* @throws {ServerError} If, with no keys cached, the JWKS endpoint returns
|
|
259
|
+
* a 4xx or a body that is not a JWKS, or discovery reports no `jwks_uri`
|
|
260
|
+
* — a misconfiguration, not a verdict on the caller's token.
|
|
261
261
|
*/
|
|
262
262
|
getToken(accessToken: string): Promise<Token<Client, User, S> | undefined>;
|
|
263
263
|
}
|
|
@@ -276,9 +276,9 @@ export class JwksTokenReader {
|
|
|
276
276
|
* available — a down issuer must be distinguishable from a genuinely
|
|
277
277
|
* invalid token, not collapsed into it. Once keys are cached, a failed
|
|
278
278
|
* refresh is absorbed and the cached keys keep serving.
|
|
279
|
-
* @throws {ServerError} If the JWKS endpoint returns
|
|
280
|
-
* is not a JWKS, or discovery reports no `jwks_uri`
|
|
281
|
-
* misconfiguration, not a verdict on the caller's token.
|
|
279
|
+
* @throws {ServerError} If, with no keys cached, the JWKS endpoint returns
|
|
280
|
+
* a 4xx or a body that is not a JWKS, or discovery reports no `jwks_uri`
|
|
281
|
+
* — a misconfiguration, not a verdict on the caller's token.
|
|
282
282
|
*/
|
|
283
283
|
async getToken(accessToken) {
|
|
284
284
|
const parsed = parseJws(accessToken);
|
|
@@ -149,15 +149,12 @@ export declare function checkRedirectUriPattern(pattern: string, isPublicSuffix?
|
|
|
149
149
|
* - A host label may never be empty, and a single trailing dot is treated as
|
|
150
150
|
* the same host, so `example.com.` cannot smuggle an extra label past the
|
|
151
151
|
* parent-domain rule.
|
|
152
|
-
* - A requested URI containing `*` never matches
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* comparison no longer matches at all. Such a registration cannot be created
|
|
159
|
-
* any more — a `*` now makes the value a pattern, which must satisfy the rules
|
|
160
|
-
* above — so this only affects rows written before patterns existed.
|
|
152
|
+
* - A requested URI containing a literal `*` never matches any registration,
|
|
153
|
+
* so a pattern can never be replayed back as a redirect target. A requested
|
|
154
|
+
* host that percent-decodes to one containing `*` never matches a pattern.
|
|
155
|
+
*
|
|
156
|
+
* Any registration containing `*` is a pattern, so a stored literal URI with a
|
|
157
|
+
* `*` in its host matches only if it also satisfies the pattern rules.
|
|
161
158
|
*
|
|
162
159
|
* @param registered The client's registered redirect URIs.
|
|
163
160
|
* @param redirectUri The `redirect_uri` from the authorization request.
|
|
@@ -38,27 +38,8 @@
|
|
|
38
38
|
*
|
|
39
39
|
* @module
|
|
40
40
|
*/
|
|
41
|
-
|
|
42
|
-
* A label used to ask whether the *children* of a parent domain are public
|
|
43
|
-
* suffixes, which is a different question from whether the parent is one.
|
|
44
|
-
*
|
|
45
|
-
* `r.appspot.com` is a registrable domain by the list's reckoning, but the rule
|
|
46
|
-
* `*.r.appspot.com` makes every one of its subdomains a public suffix belonging
|
|
47
|
-
* to a different registrant — so a wildcard over it hands codes to strangers
|
|
48
|
-
* even though the parent itself passes. Probing with a label no registry has
|
|
49
|
-
* ever issued answers that in one lookup.
|
|
50
|
-
*/
|
|
41
|
+
// A label no registry issues; a PSL hit on it catches `*.r.appspot.com` rules.
|
|
51
42
|
const CHILD_PROBE_LABEL = "udibo-public-suffix-probe";
|
|
52
|
-
/**
|
|
53
|
-
* Split a hostname into labels, or `null` when it is not usable for matching.
|
|
54
|
-
*
|
|
55
|
-
* A single trailing dot is the DNS root and is dropped, so `example.com.` and
|
|
56
|
-
* `example.com` are the same host — without this, the extra empty label would
|
|
57
|
-
* shift every label the public-suffix check looks at and let `*.deno.net.`
|
|
58
|
-
* through. Any *other* empty label (`a..b`, a leading `.`) is refused outright
|
|
59
|
-
* rather than normalized: it cannot resolve, and an empty leftmost label would
|
|
60
|
-
* otherwise satisfy any prefix/suffix test.
|
|
61
|
-
*/
|
|
62
43
|
function hostLabels(hostname) {
|
|
63
44
|
const normalized = hostname.endsWith(".") ? hostname.slice(0, -1) : hostname;
|
|
64
45
|
if (normalized.length === 0)
|
|
@@ -175,12 +156,7 @@ export function checkRedirectUriPattern(pattern, isPublicSuffix) {
|
|
|
175
156
|
}
|
|
176
157
|
return null;
|
|
177
158
|
}
|
|
178
|
-
|
|
179
|
-
* The loopback IP literals RFC 8252 §7.3 covers, in the form
|
|
180
|
-
* `URL.hostname` normalizes them to. `localhost` is deliberately absent: RFC
|
|
181
|
-
* 8252 §8.3 advises against it because its resolution depends on a host file
|
|
182
|
-
* the authorization server does not control.
|
|
183
|
-
*/
|
|
159
|
+
// RFC 8252 §7.3 IP literals only; `localhost` is excluded per RFC 8252 §8.3.
|
|
184
160
|
const LOOPBACK_HOSTNAMES = new Set(["127.0.0.1", "[::1]"]);
|
|
185
161
|
function matchesLoopback(registration, redirectUri) {
|
|
186
162
|
if (isRedirectUriPattern(registration))
|
|
@@ -275,15 +251,12 @@ function matchesPattern(pattern, redirectUri, isPublicSuffix) {
|
|
|
275
251
|
* - A host label may never be empty, and a single trailing dot is treated as
|
|
276
252
|
* the same host, so `example.com.` cannot smuggle an extra label past the
|
|
277
253
|
* parent-domain rule.
|
|
278
|
-
* - A requested URI containing `*` never matches
|
|
279
|
-
*
|
|
280
|
-
*
|
|
254
|
+
* - A requested URI containing a literal `*` never matches any registration,
|
|
255
|
+
* so a pattern can never be replayed back as a redirect target. A requested
|
|
256
|
+
* host that percent-decodes to one containing `*` never matches a pattern.
|
|
281
257
|
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* comparison no longer matches at all. Such a registration cannot be created
|
|
285
|
-
* any more — a `*` now makes the value a pattern, which must satisfy the rules
|
|
286
|
-
* above — so this only affects rows written before patterns existed.
|
|
258
|
+
* Any registration containing `*` is a pattern, so a stored literal URI with a
|
|
259
|
+
* `*` in its host matches only if it also satisfies the pattern rules.
|
|
287
260
|
*
|
|
288
261
|
* @param registered The client's registered redirect URIs.
|
|
289
262
|
* @param redirectUri The `redirect_uri` from the authorization request.
|
|
@@ -231,10 +231,12 @@ export declare class ResourceServer<Client extends ClientInterface, User, S exte
|
|
|
231
231
|
*
|
|
232
232
|
* The token's expiry is compared against now with {@linkcode
|
|
233
233
|
* clockSkewSeconds} of leeway, so the same tolerance applies whichever
|
|
234
|
-
* token reader produced the token.
|
|
234
|
+
* token reader produced the token. A token without `accessTokenExpiresAt`
|
|
235
|
+
* is never refused as expired here. Revocation is the reader's to report,
|
|
236
|
+
* by resolving no token.
|
|
235
237
|
*
|
|
236
|
-
* @throws {InvalidTokenError} If the
|
|
237
|
-
* configured leeway
|
|
238
|
+
* @throws {InvalidTokenError} If the reader resolves no token, or the token
|
|
239
|
+
* has expired beyond the configured leeway
|
|
238
240
|
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
|
|
239
241
|
*/
|
|
240
242
|
getToken(accessToken: string, services: ResourceServerServices<Client, User, S>): Promise<Token<Client, User, S>>;
|
|
@@ -258,10 +258,12 @@ export class ResourceServer {
|
|
|
258
258
|
*
|
|
259
259
|
* The token's expiry is compared against now with {@linkcode
|
|
260
260
|
* clockSkewSeconds} of leeway, so the same tolerance applies whichever
|
|
261
|
-
* token reader produced the token.
|
|
261
|
+
* token reader produced the token. A token without `accessTokenExpiresAt`
|
|
262
|
+
* is never refused as expired here. Revocation is the reader's to report,
|
|
263
|
+
* by resolving no token.
|
|
262
264
|
*
|
|
263
|
-
* @throws {InvalidTokenError} If the
|
|
264
|
-
* configured leeway
|
|
265
|
+
* @throws {InvalidTokenError} If the reader resolves no token, or the token
|
|
266
|
+
* has expired beyond the configured leeway
|
|
265
267
|
* @see https://datatracker.ietf.org/doc/html/rfc6750#section-3.1
|
|
266
268
|
*/
|
|
267
269
|
async getToken(accessToken, services) {
|
|
@@ -30,6 +30,10 @@ export interface ClientServiceInterface<Client extends ClientInterface, User> {
|
|
|
30
30
|
* carried none.
|
|
31
31
|
*/
|
|
32
32
|
getAuthenticated(id: string, secret?: string): Promise<Client | undefined>;
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Resolves the user a client-credentials token acts for. Resolving
|
|
35
|
+
* `undefined` is the conformant case and yields a user-less machine token;
|
|
36
|
+
* see `ClientCredentialsGrant` before mapping a client to a human user.
|
|
37
|
+
*/
|
|
34
38
|
getUser(client: Client | string): Promise<User | undefined>;
|
|
35
39
|
}
|
|
@@ -34,15 +34,15 @@ import type { AbstractScope } from "../../models/scope.js";
|
|
|
34
34
|
*/
|
|
35
35
|
export interface DeviceAuthorizationServiceInterface<Client extends ClientInterface, User, Scope extends AbstractScope> {
|
|
36
36
|
/**
|
|
37
|
-
* Lifetime of device codes in seconds.
|
|
38
|
-
*
|
|
39
|
-
* @default 1800 (30 minutes)
|
|
37
|
+
* Lifetime of device codes in seconds. RFC 8628 sets no default; 15–30
|
|
38
|
+
* minutes is common.
|
|
40
39
|
*/
|
|
41
40
|
lifetime: number;
|
|
42
41
|
/**
|
|
43
|
-
* Minimum polling interval in seconds
|
|
44
|
-
*
|
|
45
|
-
*
|
|
42
|
+
* Minimum polling interval in seconds, copied onto each new authorization
|
|
43
|
+
* and returned to the device as `interval`. The grant answers a faster poll
|
|
44
|
+
* with `slow_down` and adds 5 seconds to that authorization's interval
|
|
45
|
+
* (RFC 8628 §3.5).
|
|
46
46
|
*/
|
|
47
47
|
interval: number;
|
|
48
48
|
/**
|
|
@@ -24,9 +24,10 @@ export interface TokenReaderInterface<Client extends ClientInterface, User, Scop
|
|
|
24
24
|
* its grant types.
|
|
25
25
|
*
|
|
26
26
|
* **A token need not have a user.** The client credentials grant (RFC 6749
|
|
27
|
-
* §4.4) has no resource owner, so
|
|
28
|
-
*
|
|
29
|
-
* saves a {@linkcode Token}
|
|
27
|
+
* §4.4) has no resource owner, so when `ClientServiceInterface.getUser`
|
|
28
|
+
* resolves none it calls `acceptedScope`, `generateAccessToken`, and
|
|
29
|
+
* `accessTokenExpiresAt` with `user: undefined` and saves a {@linkcode Token}
|
|
30
|
+
* whose `user` is unset. An implementation that
|
|
30
31
|
* dereferences `user` without checking will throw on those requests, and a
|
|
31
32
|
* store with a non-nullable user column cannot persist the result — decide
|
|
32
33
|
* scope and lifetime from the client alone when it is absent.
|
|
@@ -53,8 +54,13 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
|
|
|
53
54
|
* generator names the client as the subject then, per RFC 9068 §2.2.
|
|
54
55
|
* Forward `authenticationContext` to a signed-token generator so it uses
|
|
55
56
|
* the credential's event, including the original event on refresh.
|
|
57
|
+
* `expiresAt` is the expiry the grant will store for this token — already
|
|
58
|
+
* clamped to the refresh family's cap when one applies. Forward it too, so
|
|
59
|
+
* a signed token's `exp` never outlives what the server itself honors; the
|
|
60
|
+
* grants always pass it, and a wrapper that drops it leaves offline
|
|
61
|
+
* verifiers accepting the token past its stored expiry.
|
|
56
62
|
*/
|
|
57
|
-
generateAccessToken(client: Client, user: User | undefined, scope?: Scope | null, authenticationContext?: AuthenticationContext): Promise<string>;
|
|
63
|
+
generateAccessToken(client: Client, user: User | undefined, scope?: Scope | null, authenticationContext?: AuthenticationContext, expiresAt?: Date): Promise<string>;
|
|
58
64
|
/** Generates a refresh token string. */
|
|
59
65
|
generateRefreshToken(client: Client, user: User, scope?: Scope | null): Promise<string | undefined>;
|
|
60
66
|
/**
|
|
@@ -71,7 +77,8 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
|
|
|
71
77
|
*
|
|
72
78
|
* **This method is the whole cap.** The grants clamp a rotation's access and
|
|
73
79
|
* refresh expiries to the date it returns and answer `invalid_grant` once
|
|
74
|
-
* that date has passed
|
|
80
|
+
* that date has passed; a refresh-token record without `familyCreatedAt` is
|
|
81
|
+
* never capped. Return `undefined` — or leave the method off
|
|
75
82
|
* entirely, which is the default — and the family is never capped, however
|
|
76
83
|
* the service is otherwise configured. Implement it to cap per client (see
|
|
77
84
|
* {@linkcode AbstractTokenService.refreshTokenMaxLifetime} for the
|
|
@@ -151,8 +158,8 @@ export interface AbstractTokenServiceOptions<Client extends ClientInterface, Use
|
|
|
151
158
|
/**
|
|
152
159
|
* Absolute lifetime of a refresh-token family in seconds, read by this
|
|
153
160
|
* class's {@linkcode AbstractTokenService.refreshTokenFamilyExpiresAt}. Omit
|
|
154
|
-
*
|
|
155
|
-
* `refreshTokenLifetime`.
|
|
161
|
+
* to leave families uncapped, so rotation extends a family indefinitely.
|
|
162
|
+
* Must be at least `refreshTokenLifetime`.
|
|
156
163
|
*/
|
|
157
164
|
refreshTokenMaxLifetime?: number;
|
|
158
165
|
/** Client service for hydrating tokens. */
|
|
@@ -202,7 +209,7 @@ export declare abstract class AbstractTokenService<Client extends ClientInterfac
|
|
|
202
209
|
*/
|
|
203
210
|
acceptedScope(_client: Client, _user: User | undefined, scope?: Scope | null): Promise<Scope | null | undefined | false>;
|
|
204
211
|
/** Generates an opaque access token string. Override to issue a different format. */
|
|
205
|
-
generateAccessToken(_client: Client, _user: User | undefined, _scope?: Scope | null, _authenticationContext?: AuthenticationContext): Promise<string>;
|
|
212
|
+
generateAccessToken(_client: Client, _user: User | undefined, _scope?: Scope | null, _authenticationContext?: AuthenticationContext, _expiresAt?: Date): Promise<string>;
|
|
206
213
|
/** Generates an opaque refresh token string. Override to issue a different format. */
|
|
207
214
|
generateRefreshToken(_client: Client, _user: User, _scope?: Scope | null): Promise<string | undefined>;
|
|
208
215
|
/** Computes the access token expiry as now plus {@linkcode accessTokenLifetime}. */
|
|
@@ -52,7 +52,7 @@ export class AbstractTokenService {
|
|
|
52
52
|
return Promise.resolve(scope);
|
|
53
53
|
}
|
|
54
54
|
/** Generates an opaque access token string. Override to issue a different format. */
|
|
55
|
-
generateAccessToken(_client, _user, _scope, _authenticationContext) {
|
|
55
|
+
generateAccessToken(_client, _user, _scope, _authenticationContext, _expiresAt) {
|
|
56
56
|
return Promise.resolve(crypto.randomUUID());
|
|
57
57
|
}
|
|
58
58
|
/** Generates an opaque refresh token string. Override to issue a different format. */
|
|
@@ -115,7 +115,11 @@ export declare function generateSigningKey(): Promise<SigningKey>;
|
|
|
115
115
|
export declare function exportSigningKeyJwk(key: SigningKey): Promise<JsonWebKey & {
|
|
116
116
|
kid: string;
|
|
117
117
|
}>;
|
|
118
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* Import a signing key previously exported with {@link exportSigningKeyJwk}.
|
|
120
|
+
* A JWK without a `kid` is given a random one, so every instance importing it
|
|
121
|
+
* would publish and stamp a different `kid` — persist the exported `kid`.
|
|
122
|
+
*/
|
|
119
123
|
export declare function importSigningKeyJwk(jwk: JsonWebKey & {
|
|
120
124
|
kid?: string;
|
|
121
125
|
}): Promise<SigningKey>;
|
|
@@ -139,10 +143,12 @@ export interface VerifyJwtOptions {
|
|
|
139
143
|
}
|
|
140
144
|
/**
|
|
141
145
|
* Verify a compact JWS against a public JWK and return its payload, or
|
|
142
|
-
* `undefined` when the signature (or shape) is invalid or
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
+
* `undefined` when the signature (or shape) is invalid or a numeric `exp` is
|
|
147
|
+
* not in the future. It verifies ES256 regardless of the header's `alg` and
|
|
148
|
+
* checks nothing else: a payload with no `exp` passes, and `iss`, `aud`, and
|
|
149
|
+
* `nbf` are the caller's to check. Exported for tests and lightweight
|
|
150
|
+
* in-process verification; production resource servers should verify against
|
|
151
|
+
* the JWKS endpoint with their JOSE library of choice.
|
|
146
152
|
*/
|
|
147
153
|
export declare function verifyJwt(jwt: string, publicJwk: JsonWebKey, options?: VerifyJwtOptions): Promise<Record<string, unknown> | undefined>;
|
|
148
154
|
/**
|
|
@@ -159,8 +165,10 @@ export declare function verifyJwt(jwt: string, publicJwk: JsonWebKey, options?:
|
|
|
159
165
|
* }
|
|
160
166
|
* ```
|
|
161
167
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
168
|
+
* Your token store persists the JWT string like any access token, so
|
|
169
|
+
* revocation and introspection work as with opaque tokens for readers that
|
|
170
|
+
* consult the store. A resource server that verifies the JWT offline against
|
|
171
|
+
* JWKS does not see a revocation and accepts the token until its `exp`.
|
|
164
172
|
*/
|
|
165
173
|
export declare function createJwtAccessTokenGenerator(options: {
|
|
166
174
|
/** Keys the tokens are signed with (also served from JWKS). */
|
|
@@ -173,7 +181,14 @@ export declare function createJwtAccessTokenGenerator(options: {
|
|
|
173
181
|
* the client id, which single-resource validators must then accept.
|
|
174
182
|
*/
|
|
175
183
|
audience?: string;
|
|
176
|
-
/**
|
|
184
|
+
/**
|
|
185
|
+
* Upper bound in seconds on the `exp` claim. Defaults to 3600. The claim
|
|
186
|
+
* is the earlier of this and the expiry the grant passes as the fifth
|
|
187
|
+
* argument — the token service's `accessTokenExpiresAt`, already clamped to
|
|
188
|
+
* any refresh-family cap — so an offline verifier never accepts the token
|
|
189
|
+
* past the server's own expiry. A wrapper that drops that argument falls
|
|
190
|
+
* back to this lifetime alone.
|
|
191
|
+
*/
|
|
177
192
|
lifetimeSeconds?: number;
|
|
178
193
|
/** Maps a user to the `sub` claim. Defaults to the user's `id` property. */
|
|
179
194
|
subjectOf?: (user: unknown) => string;
|
|
@@ -198,4 +213,4 @@ export declare function createJwtAccessTokenGenerator(options: {
|
|
|
198
213
|
id: string;
|
|
199
214
|
}, user: unknown, scope?: {
|
|
200
215
|
toString(): string;
|
|
201
|
-
} | null, authenticationContext?: AuthenticationContext) => Promise<string>;
|
|
216
|
+
} | null, authenticationContext?: AuthenticationContext, expiresAt?: Date) => Promise<string>;
|
|
@@ -128,7 +128,11 @@ export async function exportSigningKeyJwk(key) {
|
|
|
128
128
|
const jwk = await crypto.subtle.exportKey("jwk", key.privateKey);
|
|
129
129
|
return { ...jwk, kid: key.kid, alg: key.alg };
|
|
130
130
|
}
|
|
131
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Import a signing key previously exported with {@link exportSigningKeyJwk}.
|
|
133
|
+
* A JWK without a `kid` is given a random one, so every instance importing it
|
|
134
|
+
* would publish and stamp a different `kid` — persist the exported `kid`.
|
|
135
|
+
*/
|
|
132
136
|
export async function importSigningKeyJwk(jwk) {
|
|
133
137
|
const privateKey = await crypto.subtle.importKey("jwk", jwk, ES256_PARAMS, true, ["sign"]);
|
|
134
138
|
const kid = jwk.kid ?? crypto.randomUUID();
|
|
@@ -150,10 +154,12 @@ export async function signJwt(key, payload, header = {}) {
|
|
|
150
154
|
}
|
|
151
155
|
/**
|
|
152
156
|
* Verify a compact JWS against a public JWK and return its payload, or
|
|
153
|
-
* `undefined` when the signature (or shape) is invalid or
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
+
* `undefined` when the signature (or shape) is invalid or a numeric `exp` is
|
|
158
|
+
* not in the future. It verifies ES256 regardless of the header's `alg` and
|
|
159
|
+
* checks nothing else: a payload with no `exp` passes, and `iss`, `aud`, and
|
|
160
|
+
* `nbf` are the caller's to check. Exported for tests and lightweight
|
|
161
|
+
* in-process verification; production resource servers should verify against
|
|
162
|
+
* the JWKS endpoint with their JOSE library of choice.
|
|
157
163
|
*/
|
|
158
164
|
export async function verifyJwt(jwt, publicJwk, options = {}) {
|
|
159
165
|
const parts = jwt.split(".");
|
|
@@ -190,13 +196,15 @@ export async function verifyJwt(jwt, publicJwk, options = {}) {
|
|
|
190
196
|
* }
|
|
191
197
|
* ```
|
|
192
198
|
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
199
|
+
* Your token store persists the JWT string like any access token, so
|
|
200
|
+
* revocation and introspection work as with opaque tokens for readers that
|
|
201
|
+
* consult the store. A resource server that verifies the JWT offline against
|
|
202
|
+
* JWKS does not see a revocation and accepts the token until its `exp`.
|
|
195
203
|
*/
|
|
196
204
|
export function createJwtAccessTokenGenerator(options) {
|
|
197
205
|
const lifetime = options.lifetimeSeconds ?? 3600;
|
|
198
206
|
const subjectOf = options.subjectOf ?? defaultSubjectOf;
|
|
199
|
-
return async (client, user, scope, authenticationContext) => {
|
|
207
|
+
return async (client, user, scope, authenticationContext, expiresAt) => {
|
|
200
208
|
const event = user
|
|
201
209
|
? snapshotAuthenticationContext(authenticationContext)
|
|
202
210
|
: undefined;
|
|
@@ -205,6 +213,9 @@ export function createJwtAccessTokenGenerator(options) {
|
|
|
205
213
|
const extraClaims = user && options.userClaims
|
|
206
214
|
? await options.userClaims(user, scope, client, event)
|
|
207
215
|
: undefined;
|
|
216
|
+
const storedExp = expiresAt
|
|
217
|
+
? Math.floor(expiresAt.getTime() / 1000)
|
|
218
|
+
: now + lifetime;
|
|
208
219
|
const payload = {
|
|
209
220
|
...extraClaims,
|
|
210
221
|
iss: options.issuer,
|
|
@@ -212,7 +223,7 @@ export function createJwtAccessTokenGenerator(options) {
|
|
|
212
223
|
client_id: client.id,
|
|
213
224
|
aud: options.audience ?? client.id,
|
|
214
225
|
iat: now,
|
|
215
|
-
exp: now + lifetime,
|
|
226
|
+
exp: Math.min(now + lifetime, storedExp),
|
|
216
227
|
jti: crypto.randomUUID(),
|
|
217
228
|
};
|
|
218
229
|
const scopeText = scope?.toString();
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* sequential case and fails the concurrent one, which is the whole
|
|
20
20
|
* reason the suites exist.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
* methods aren't part of the public service
|
|
24
|
-
* tells the runner how to populate test data.
|
|
22
|
+
* The service runners take a factory plus seed helpers because `add` /
|
|
23
|
+
* `register` methods aren't part of the public service interfaces — the
|
|
24
|
+
* consumer tells the runner how to populate test data.
|
|
25
25
|
*
|
|
26
26
|
* @module
|
|
27
27
|
*/
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* sequential case and fails the concurrent one, which is the whole
|
|
20
20
|
* reason the suites exist.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
* methods aren't part of the public service
|
|
24
|
-
* tells the runner how to populate test data.
|
|
22
|
+
* The service runners take a factory plus seed helpers because `add` /
|
|
23
|
+
* `register` methods aren't part of the public service interfaces — the
|
|
24
|
+
* consumer tells the runner how to populate test data.
|
|
25
25
|
*
|
|
26
26
|
* @module
|
|
27
27
|
*/
|
package/esm/testing/mod.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Testing helpers for apps that use `@udibo/oauth2`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three layers ship:
|
|
5
5
|
*
|
|
6
6
|
* - **Testing surface for the OAuth2 protocol.** The recommended pattern
|
|
7
7
|
* for testing apps that use the resource server is to expose your
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* - **In-memory service implementations.** `MemoryUserService`,
|
|
21
21
|
* `MemoryClientService`, `MemoryTokenService`,
|
|
22
22
|
* `MemoryAuthorizationCodeService`, and `MemoryDeviceAuthorizationService`
|
|
23
|
-
*
|
|
23
|
+
* implement the authorization server's user, client, token,
|
|
24
|
+
* authorization-code and device-authorization service interfaces, generic
|
|
25
|
+
* over the consumer's
|
|
24
26
|
* user / client / scope types. Suitable for tests, examples, and
|
|
25
27
|
* prototypes — never for production.
|
|
26
28
|
*
|
package/esm/testing/mod.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Testing helpers for apps that use `@udibo/oauth2`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Three layers ship:
|
|
5
5
|
*
|
|
6
6
|
* - **Testing surface for the OAuth2 protocol.** The recommended pattern
|
|
7
7
|
* for testing apps that use the resource server is to expose your
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* - **In-memory service implementations.** `MemoryUserService`,
|
|
21
21
|
* `MemoryClientService`, `MemoryTokenService`,
|
|
22
22
|
* `MemoryAuthorizationCodeService`, and `MemoryDeviceAuthorizationService`
|
|
23
|
-
*
|
|
23
|
+
* implement the authorization server's user, client, token,
|
|
24
|
+
* authorization-code and device-authorization service interfaces, generic
|
|
25
|
+
* over the consumer's
|
|
24
26
|
* user / client / scope types. Suitable for tests, examples, and
|
|
25
27
|
* prototypes — never for production.
|
|
26
28
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-memory implementations of
|
|
2
|
+
* In-memory implementations of the authorization server's service interfaces.
|
|
3
3
|
*
|
|
4
4
|
* The `Memory*Service` family is the public, generic version of the fixtures
|
|
5
5
|
* the library uses for its own tests. They satisfy the
|
package/esm/testing/services.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* In-memory implementations of
|
|
2
|
+
* In-memory implementations of the authorization server's service interfaces.
|
|
3
3
|
*
|
|
4
4
|
* The `Memory*Service` family is the public, generic version of the fixtures
|
|
5
5
|
* the library uses for its own tests. They satisfy the
|
package/esm/testing/tenant.d.ts
CHANGED
|
@@ -124,9 +124,17 @@ export interface FakeTenant {
|
|
|
124
124
|
addUser(user: FakeTenantUser): Promise<void>;
|
|
125
125
|
/** Adds an organization. Throws on a duplicate id. */
|
|
126
126
|
addOrganization(organization: FakeTenantOrganization): void;
|
|
127
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Makes a person a member of an organization, replacing any earlier
|
|
129
|
+
* membership. Throws when the organization does not exist.
|
|
130
|
+
*/
|
|
128
131
|
addMember(organizationId: string, userId: string, membership?: FakeTenantMembership): void;
|
|
129
|
-
/**
|
|
132
|
+
/**
|
|
133
|
+
* Ends a membership. From then on introspection and `/api/check` stop
|
|
134
|
+
* answering for that organization on credentials issued in it, and UserInfo
|
|
135
|
+
* drops its `org_*` claims; a JWT access token or id_token already minted
|
|
136
|
+
* keeps its claims until it expires.
|
|
137
|
+
*/
|
|
130
138
|
removeMember(organizationId: string, userId: string): void;
|
|
131
139
|
/** Registers a resource type, so `/api/check` accepts it. */
|
|
132
140
|
registerResourceType(type: string): void;
|
|
@@ -134,12 +142,13 @@ export interface FakeTenant {
|
|
|
134
142
|
grant(grant: FakeTenantGrant): void;
|
|
135
143
|
/**
|
|
136
144
|
* Chooses who the next authorization requests authenticate as, until
|
|
137
|
-
* called again. With nobody chosen, authorize answers `
|
|
145
|
+
* called again. With nobody chosen, authorize answers `access_denied`.
|
|
138
146
|
*/
|
|
139
147
|
signInAs(userId: string | null, signIn?: FakeTenantSignIn): void;
|
|
140
148
|
/**
|
|
141
149
|
* Mints an access token without the browser flow, for testing an API
|
|
142
|
-
* directly. JWT or opaque as the client is registered.
|
|
150
|
+
* directly. JWT or opaque as the client is registered. Throws when the
|
|
151
|
+
* client or user does not exist.
|
|
143
152
|
*/
|
|
144
153
|
issueAccessToken(request: FakeTenantTokenRequest): Promise<string>;
|
|
145
154
|
}
|
package/esm/utils/_buffer.d.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Copies the viewed bytes into a standalone `ArrayBuffer`, for passing a
|
|
8
|
+
* `Uint8Array<ArrayBufferLike>` where Web Crypto's `BufferSource` type does not
|
|
9
|
+
* accept it.
|
|
9
10
|
*/
|
|
10
11
|
export declare function toArrayBuffer(bytes: Uint8Array): ArrayBuffer;
|