@udibo/oauth2 0.23.0 → 0.25.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/adapters/hono/bff/bff.d.ts +4 -2
- package/dist/adapters/hono/bff/bff.js +28 -9
- package/dist/adapters/hono/bff/mod.d.ts +1 -1
- package/dist/adapters/hono/bff/session-store.d.ts +85 -12
- package/dist/adapters/hono/bff/session-store.js +105 -11
- package/dist/adapters/hono/bff/testing.d.ts +6 -2
- package/dist/adapters/hono/bff/testing.js +96 -2
- package/dist/client/direct-client.d.ts +2 -2
- package/dist/client/discovery-cache.d.ts +3 -3
- package/dist/identity/captcha.d.ts +2 -2
- package/dist/models/authorization.d.ts +5 -5
- package/dist/models/authorization.js +2 -2
- package/dist/models/client.d.ts +1 -1
- package/dist/react/components/class-names.d.ts +2 -2
- package/dist/react/components/class-names.js +2 -2
- package/dist/server/authorization-server.d.ts +14 -11
- package/dist/server/authorization-server.js +9 -8
- package/dist/server/grants/refresh-token.d.ts +8 -4
- package/dist/server/grants/refresh-token.js +15 -4
- package/dist/server/resource-server.d.ts +10 -10
- package/dist/server/resource-server.js +3 -4
- package/dist/server/services/token.d.ts +37 -3
- package/dist/testing/contract/mod.d.ts +7 -0
- package/dist/testing/contract/mod.js +7 -0
- package/dist/testing/contract/token-rotation.d.ts +95 -0
- package/dist/testing/contract/token-rotation.js +383 -0
- package/dist/testing/contract/token.d.ts +3 -0
- package/dist/testing/contract/token.js +44 -2
- package/dist/testing/services.d.ts +7 -0
- package/dist/testing/services.js +43 -14
- package/package.json +1 -1
|
@@ -32,12 +32,12 @@ import type { AuthFormClassNames } from "./types.ts";
|
|
|
32
32
|
*
|
|
33
33
|
* @example Theme a custom field to match the form around it
|
|
34
34
|
* ```tsx
|
|
35
|
-
* function
|
|
35
|
+
* function InviteCodeField(props: { fieldProps: AuthFieldProps }) {
|
|
36
36
|
* const classNames = useAuthFormClassNames();
|
|
37
37
|
* return (
|
|
38
38
|
* <div className={classNames.field}>
|
|
39
39
|
* <label className={classNames.label} htmlFor={props.fieldProps.id}>
|
|
40
|
-
*
|
|
40
|
+
* Invite code
|
|
41
41
|
* </label>
|
|
42
42
|
* <input {...props.fieldProps} className={classNames.input} />
|
|
43
43
|
* </div>
|
|
@@ -49,12 +49,12 @@ function slotKey(classNames) {
|
|
|
49
49
|
*
|
|
50
50
|
* @example Theme a custom field to match the form around it
|
|
51
51
|
* ```tsx
|
|
52
|
-
* function
|
|
52
|
+
* function InviteCodeField(props: { fieldProps: AuthFieldProps }) {
|
|
53
53
|
* const classNames = useAuthFormClassNames();
|
|
54
54
|
* return (
|
|
55
55
|
* <div className={classNames.field}>
|
|
56
56
|
* <label className={classNames.label} htmlFor={props.fieldProps.id}>
|
|
57
|
-
*
|
|
57
|
+
* Invite code
|
|
58
58
|
* </label>
|
|
59
59
|
* <input {...props.fieldProps} className={classNames.input} />
|
|
60
60
|
* </div>
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* - Server metadata (RFC 8414)
|
|
10
10
|
* - Device authorization (RFC 8628)
|
|
11
11
|
*
|
|
12
|
-
* **Configuration.**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* **Configuration.** The only configuration seam is `resolve(request)`, which
|
|
13
|
+
* returns the per-request configuration: the services the server's own
|
|
14
|
+
* endpoints need (`{ clientService, tokenService }`) plus the issuer and
|
|
15
|
+
* endpoint URLs. Because it runs per request, one server can serve the origins
|
|
16
|
+
* or applications a business operates. Each grant resolves its own services
|
|
17
|
+
* independently. For a single deployment, `resolve` is a constant function
|
|
18
|
+
* (create the services once, outside it, and return them every time).
|
|
18
19
|
*
|
|
19
20
|
* @see https://datatracker.ietf.org/doc/html/rfc6749
|
|
20
21
|
* @module
|
|
@@ -161,9 +162,11 @@ export interface AuthorizationServerContext<Client extends ClientInterface, User
|
|
|
161
162
|
*/
|
|
162
163
|
export interface AuthorizationServerOptions<Client extends ClientInterface, User, S extends AbstractScope> extends Omit<ResourceServerOptions<Client, User, S>, "resolve"> {
|
|
163
164
|
/**
|
|
164
|
-
* Resolves the per-request
|
|
165
|
-
* issuer
|
|
166
|
-
*
|
|
165
|
+
* Resolves the per-request configuration — the server's own services plus
|
|
166
|
+
* the issuer and endpoint URLs — so one server can serve the origins or
|
|
167
|
+
* applications a business operates. This is the only services seam: for a
|
|
168
|
+
* single deployment it is a constant function returning the same context
|
|
169
|
+
* every time.
|
|
167
170
|
*/
|
|
168
171
|
resolve: (request: Request) => AuthorizationServerContext<Client, User, S> | Promise<AuthorizationServerContext<Client, User, S>>;
|
|
169
172
|
/** Grants the server supports, keyed by `grant_type` (each key must equal `grant.grantType`). */
|
|
@@ -406,8 +409,8 @@ export declare const ENDPOINT_METHODS: Readonly<Record<EndpointKey, readonly str
|
|
|
406
409
|
* Framework-agnostic: each `handle*Request` method takes a web `Request` and
|
|
407
410
|
* returns a `Response`, so an adapter routes its endpoints to them. Extends
|
|
408
411
|
* {@linkcode ResourceServer}, so the same instance also validates bearer
|
|
409
|
-
* tokens.
|
|
410
|
-
* callback in {@linkcode AuthorizationServerOptions}.
|
|
412
|
+
* tokens. Its issuer, endpoint URLs and services come per request from the
|
|
413
|
+
* `resolve` callback in {@linkcode AuthorizationServerOptions}.
|
|
411
414
|
*
|
|
412
415
|
* The handlers throw {@linkcode HttpError} subclasses on failure (e.g.
|
|
413
416
|
* {@linkcode InvalidRequestError}, {@linkcode InvalidClientError},
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* - Server metadata (RFC 8414)
|
|
10
10
|
* - Device authorization (RFC 8628)
|
|
11
11
|
*
|
|
12
|
-
* **Configuration.**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* **Configuration.** The only configuration seam is `resolve(request)`, which
|
|
13
|
+
* returns the per-request configuration: the services the server's own
|
|
14
|
+
* endpoints need (`{ clientService, tokenService }`) plus the issuer and
|
|
15
|
+
* endpoint URLs. Because it runs per request, one server can serve the origins
|
|
16
|
+
* or applications a business operates. Each grant resolves its own services
|
|
17
|
+
* independently. For a single deployment, `resolve` is a constant function
|
|
18
|
+
* (create the services once, outside it, and return them every time).
|
|
18
19
|
*
|
|
19
20
|
* @see https://datatracker.ietf.org/doc/html/rfc6749
|
|
20
21
|
* @module
|
|
@@ -154,8 +155,8 @@ function isDeviceAuthorizationGrant(grant) {
|
|
|
154
155
|
* Framework-agnostic: each `handle*Request` method takes a web `Request` and
|
|
155
156
|
* returns a `Response`, so an adapter routes its endpoints to them. Extends
|
|
156
157
|
* {@linkcode ResourceServer}, so the same instance also validates bearer
|
|
157
|
-
* tokens.
|
|
158
|
-
* callback in {@linkcode AuthorizationServerOptions}.
|
|
158
|
+
* tokens. Its issuer, endpoint URLs and services come per request from the
|
|
159
|
+
* `resolve` callback in {@linkcode AuthorizationServerOptions}.
|
|
159
160
|
*
|
|
160
161
|
* The handlers throw {@linkcode HttpError} subclasses on failure (e.g.
|
|
161
162
|
* {@linkcode InvalidRequestError}, {@linkcode InvalidClientError},
|
|
@@ -86,10 +86,14 @@ export declare class RefreshTokenGrant<Client extends ClientInterface, User, S e
|
|
|
86
86
|
* token (and rotates the refresh token when the token service supports it)
|
|
87
87
|
* from a valid, unexpired refresh token.
|
|
88
88
|
*
|
|
89
|
-
* Rotation
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
*
|
|
89
|
+
* Rotation claims the exchanged token and saves its successor in one call
|
|
90
|
+
* to the token service's `rotate` when implemented. Otherwise it takes two
|
|
91
|
+
* calls: it revokes the exchanged token through `revokeRotated` when
|
|
92
|
+
* implemented, falling back to `revoke` — so a service whose `revoke` ends
|
|
93
|
+
* the owning session can keep this bookkeeping step from signing the
|
|
94
|
+
* person out — and then saves the successor. A refresh that presents the
|
|
95
|
+
* exchanged token between those two calls is treated as reuse
|
|
96
|
+
* ({@linkcode TokenServiceInterface.rotate} describes the cost).
|
|
93
97
|
*
|
|
94
98
|
* Rotation renews the refresh token's sliding lifetime, bounded by the
|
|
95
99
|
* family's absolute cap when the token service implements
|
|
@@ -32,10 +32,14 @@ export class RefreshTokenGrant extends AbstractGrant {
|
|
|
32
32
|
* token (and rotates the refresh token when the token service supports it)
|
|
33
33
|
* from a valid, unexpired refresh token.
|
|
34
34
|
*
|
|
35
|
-
* Rotation
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* Rotation claims the exchanged token and saves its successor in one call
|
|
36
|
+
* to the token service's `rotate` when implemented. Otherwise it takes two
|
|
37
|
+
* calls: it revokes the exchanged token through `revokeRotated` when
|
|
38
|
+
* implemented, falling back to `revoke` — so a service whose `revoke` ends
|
|
39
|
+
* the owning session can keep this bookkeeping step from signing the
|
|
40
|
+
* person out — and then saves the successor. A refresh that presents the
|
|
41
|
+
* exchanged token between those two calls is treated as reuse
|
|
42
|
+
* ({@linkcode TokenServiceInterface.rotate} describes the cost).
|
|
39
43
|
*
|
|
40
44
|
* Rotation renews the refresh token's sliding lifetime, bounded by the
|
|
41
45
|
* family's absolute cap when the token service implements
|
|
@@ -122,6 +126,13 @@ export class RefreshTokenGrant extends AbstractGrant {
|
|
|
122
126
|
if (familyExpiresAt) {
|
|
123
127
|
this.capAtFamilyExpiry(nextRefreshToken, familyExpiresAt);
|
|
124
128
|
}
|
|
129
|
+
if (tokenService.rotate) {
|
|
130
|
+
const rotated = await tokenService.rotate(currentToken, nextRefreshToken);
|
|
131
|
+
if (!rotated) {
|
|
132
|
+
throw new InvalidGrantError("refresh_token was already consumed");
|
|
133
|
+
}
|
|
134
|
+
return rotated;
|
|
135
|
+
}
|
|
125
136
|
const claimed = tokenService.revokeRotated
|
|
126
137
|
? await tokenService.revokeRotated(currentToken)
|
|
127
138
|
: await tokenService.revoke(currentToken);
|
|
@@ -22,17 +22,18 @@ export interface ResourceServerServices<Client extends ClientInterface, User, S
|
|
|
22
22
|
}
|
|
23
23
|
/** Per-request context resolved by a {@link ResourceServer}. */
|
|
24
24
|
export interface ResourceServerContext<Client extends ClientInterface, User, S extends AbstractScope> {
|
|
25
|
-
/**
|
|
25
|
+
/** The services to use for this request. */
|
|
26
26
|
services: ResourceServerServices<Client, User, S>;
|
|
27
27
|
}
|
|
28
28
|
/** Options for configuring the resource server. */
|
|
29
29
|
export interface ResourceServerOptions<Client extends ClientInterface, User, S extends AbstractScope> {
|
|
30
30
|
/**
|
|
31
|
-
* Resolves the per-request
|
|
32
|
-
* ResourceServerServices} for THIS request — so
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* (create the services once, outside `resolve`,
|
|
31
|
+
* Resolves the per-request configuration — the {@link
|
|
32
|
+
* ResourceServerServices} for THIS request — so one server can serve the
|
|
33
|
+
* origins or applications a business operates. This is the only services
|
|
34
|
+
* seam: for a single deployment it is a constant function returning the
|
|
35
|
+
* same services every time (create the services once, outside `resolve`,
|
|
36
|
+
* and return them here).
|
|
36
37
|
*/
|
|
37
38
|
resolve: (request: Request) => ResourceServerContext<Client, User, S> | Promise<ResourceServerContext<Client, User, S>>;
|
|
38
39
|
/** Scope constructor used to parse and compare scopes. Defaults to {@linkcode BasicScope}. */
|
|
@@ -133,10 +134,9 @@ export interface AuthenticatedContext<Client extends ClientInterface, User, S ex
|
|
|
133
134
|
* authentication. Framework-agnostic: it works on web `Request`/`Response`, so
|
|
134
135
|
* an adapter (Hono, etc.) wires {@linkcode authenticate} into its middleware.
|
|
135
136
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* services every time.
|
|
137
|
+
* Per-request services come from the `resolve` callback in
|
|
138
|
+
* {@linkcode ResourceServerOptions}. For a single deployment, `resolve` is a
|
|
139
|
+
* constant function returning the same services every time.
|
|
140
140
|
*
|
|
141
141
|
* {@linkcode authenticate} throws {@linkcode HttpError} subclasses on failure
|
|
142
142
|
* ({@linkcode AccessDeniedError} when no token is sent,
|
|
@@ -31,10 +31,9 @@ const CHALLENGE_ERROR_CODES = new Set([
|
|
|
31
31
|
* authentication. Framework-agnostic: it works on web `Request`/`Response`, so
|
|
32
32
|
* an adapter (Hono, etc.) wires {@linkcode authenticate} into its middleware.
|
|
33
33
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* services every time.
|
|
34
|
+
* Per-request services come from the `resolve` callback in
|
|
35
|
+
* {@linkcode ResourceServerOptions}. For a single deployment, `resolve` is a
|
|
36
|
+
* constant function returning the same services every time.
|
|
38
37
|
*
|
|
39
38
|
* {@linkcode authenticate} throws {@linkcode HttpError} subclasses on failure
|
|
40
39
|
* ({@linkcode AccessDeniedError} when no token is sent,
|
|
@@ -129,6 +129,32 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
|
|
|
129
129
|
* it, so a sign-out racing a refresh still ends the successor.
|
|
130
130
|
*/
|
|
131
131
|
revokeRotated?(token: RefreshToken<Client, User, Scope>): Promise<boolean>;
|
|
132
|
+
/**
|
|
133
|
+
* Rotates a refresh token in one atomic step: claims `current` and persists
|
|
134
|
+
* `next`, its successor in the same family, or does neither. Resolves the
|
|
135
|
+
* saved successor, or `undefined` with nothing persisted when `current` was
|
|
136
|
+
* already claimed or revoked, or its family has been revoked. Concurrent
|
|
137
|
+
* calls for one `current` produce exactly one successor.
|
|
138
|
+
*
|
|
139
|
+
* When implemented, the refresh grant calls it instead of
|
|
140
|
+
* {@linkcode revokeRotated} (or {@linkcode revoke}) followed by
|
|
141
|
+
* {@linkcode save}. Without it, the grant claims and saves in two calls,
|
|
142
|
+
* and a request that presents `current` between them finds it revoked with
|
|
143
|
+
* no live successor: reuse detection revokes the family and fires
|
|
144
|
+
* `onTokenReuse`, so a benign concurrent refresh signs the person out, and a
|
|
145
|
+
* store whose `revokeFamily` does not keep the family revoked leaves the
|
|
146
|
+
* successor live.
|
|
147
|
+
*
|
|
148
|
+
* Treat the claim as a rotation, never an end of grant: stamp the
|
|
149
|
+
* rotated-out record with {@linkcode RefreshToken.rotatedAt} exactly as
|
|
150
|
+
* {@linkcode revokeRotated} does, keep it findable through
|
|
151
|
+
* {@linkcode getRevokedRefreshToken}, and revoke its access token as
|
|
152
|
+
* `revokeRotated` would. Make it linearizable with
|
|
153
|
+
* {@linkcode revokeFamily}, as that method requires.
|
|
154
|
+
* `runTokenRotationContractTests` checks it under concurrent callers, with
|
|
155
|
+
* the store's barrier wired between its claim and its insert.
|
|
156
|
+
*/
|
|
157
|
+
rotate?(current: RefreshToken<Client, User, Scope>, next: RefreshToken<Client, User, Scope>): Promise<RefreshToken<Client, User, Scope> | undefined>;
|
|
132
158
|
/**
|
|
133
159
|
* Revokes all tokens issued with the given authorization code.
|
|
134
160
|
* Returns true if any tokens were revoked (indicating code replay).
|
|
@@ -164,9 +190,17 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
|
|
|
164
190
|
* `/revoke` ends the family without firing `onTokenReuse`, so any audit
|
|
165
191
|
* trail for that case has to come from this method.
|
|
166
192
|
*
|
|
167
|
-
*
|
|
168
|
-
* after this call must be stored already revoked,
|
|
169
|
-
* `MemoryTokenService` does.
|
|
193
|
+
* Without {@linkcode rotate}, keep the family revoked: a successor that a
|
|
194
|
+
* concurrent rotation saves after this call must be stored already revoked,
|
|
195
|
+
* as `MemoryTokenService` does. A store that implements `rotate` meets this
|
|
196
|
+
* through the linearizability below, since the refresh grant then never
|
|
197
|
+
* saves a rotation's successor separately.
|
|
198
|
+
*
|
|
199
|
+
* With {@linkcode rotate}, make the two linearizable: once this resolves,
|
|
200
|
+
* the family has no live member and no later `rotate` of a member
|
|
201
|
+
* succeeds, and a `rotate` that committed before this began has its
|
|
202
|
+
* successor revoked. A store whose `rotate` can be paused between its claim
|
|
203
|
+
* and its insert meets this by taking one per-family lock in both methods.
|
|
170
204
|
*/
|
|
171
205
|
revokeFamily?(familyId: string): Promise<boolean>;
|
|
172
206
|
/**
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
* also have separate suites. They use app-owned fixture helpers to verify
|
|
25
25
|
* conditional credential writes and immediate, user-scoped session revocation.
|
|
26
26
|
*
|
|
27
|
+
* `TokenServiceInterface.rotate` has a runner of its own for concurrent
|
|
28
|
+
* schedules: two rotations of one refresh token, a family revocation racing a
|
|
29
|
+
* rotation, and a replay racing a refresh. A store that awaits between its
|
|
30
|
+
* claim and its insert wires `TokenRotationBarriers` into `rotate`, so the
|
|
31
|
+
* suite can hold a rotation there.
|
|
32
|
+
*
|
|
27
33
|
* The service runners take a factory plus seed helpers because `add` /
|
|
28
34
|
* `register` methods aren't part of the public service interfaces — the
|
|
29
35
|
* consumer tells the runner how to populate test data.
|
|
@@ -33,6 +39,7 @@
|
|
|
33
39
|
export { runUserServiceContractTests, type UserServiceContractOptions, } from "./user.ts";
|
|
34
40
|
export { type ClientServiceContractOptions, runClientServiceContractTests, } from "./client.ts";
|
|
35
41
|
export { runTokenServiceContractTests, type TokenServiceContractOptions, } from "./token.ts";
|
|
42
|
+
export { runTokenRotationContractTests, type TokenRotationBarriers, type TokenRotationContractOptions, type TokenRotationContractServices, } from "./token-rotation.ts";
|
|
36
43
|
export { runTokenReaderContractTests, type TokenReaderContractFixture, type TokenReaderContractOptions, } from "./token-reader.ts";
|
|
37
44
|
export { type AuthorizationCodeServiceContractOptions, runAuthorizationCodeServiceContractTests, } from "./authorization-code.ts";
|
|
38
45
|
export { type DeviceAuthorizationServiceContractOptions, runDeviceAuthorizationServiceContractTests, } from "./device-authorization.ts";
|
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
* also have separate suites. They use app-owned fixture helpers to verify
|
|
25
25
|
* conditional credential writes and immediate, user-scoped session revocation.
|
|
26
26
|
*
|
|
27
|
+
* `TokenServiceInterface.rotate` has a runner of its own for concurrent
|
|
28
|
+
* schedules: two rotations of one refresh token, a family revocation racing a
|
|
29
|
+
* rotation, and a replay racing a refresh. A store that awaits between its
|
|
30
|
+
* claim and its insert wires `TokenRotationBarriers` into `rotate`, so the
|
|
31
|
+
* suite can hold a rotation there.
|
|
32
|
+
*
|
|
27
33
|
* The service runners take a factory plus seed helpers because `add` /
|
|
28
34
|
* `register` methods aren't part of the public service interfaces — the
|
|
29
35
|
* consumer tells the runner how to populate test data.
|
|
@@ -33,6 +39,7 @@
|
|
|
33
39
|
export { runUserServiceContractTests, } from "./user.js";
|
|
34
40
|
export { runClientServiceContractTests, } from "./client.js";
|
|
35
41
|
export { runTokenServiceContractTests, } from "./token.js";
|
|
42
|
+
export { runTokenRotationContractTests, } from "./token-rotation.js";
|
|
36
43
|
export { runTokenReaderContractTests, } from "./token-reader.js";
|
|
37
44
|
export { runAuthorizationCodeServiceContractTests, } from "./authorization-code.js";
|
|
38
45
|
export { runDeviceAuthorizationServiceContractTests, } from "./device-authorization.js";
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link TokenServiceInterface.rotate} and its
|
|
3
|
+
* linearizability with {@link TokenServiceInterface.revokeFamily}.
|
|
4
|
+
*
|
|
5
|
+
* `runTokenServiceContractTests` checks what one caller sees. This suite
|
|
6
|
+
* checks what concurrent callers see: two rotations of one refresh token, a
|
|
7
|
+
* family revocation racing a rotation, a replay racing a refresh through the
|
|
8
|
+
* refresh grant, a rotation in an unrelated family, and a rotation whose
|
|
9
|
+
* insert fails. Each case asserts the store's end state, the live tokens of
|
|
10
|
+
* the family, not only the value each call resolved.
|
|
11
|
+
*
|
|
12
|
+
* The store under test awaits {@link TokenRotationBarriers.afterClaim} inside
|
|
13
|
+
* `rotate`, between its claim and its insert, so the suite can hold a rotation
|
|
14
|
+
* there and race the other call against it; a store that never reaches the
|
|
15
|
+
* barrier fails every held case. Only a store whose `rotate` never awaits
|
|
16
|
+
* between its claim and its insert, such as `MemoryTokenService`, opts out with
|
|
17
|
+
* `rotateAwaitsBarrier: false`; the suite then runs those schedules as
|
|
18
|
+
* overlapping calls.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import type { ClientInterface } from "../../models/client.ts";
|
|
23
|
+
import type { AbstractScope, BasicScope } from "../../models/scope.ts";
|
|
24
|
+
import type { ClientServiceInterface } from "../../server/services/client.ts";
|
|
25
|
+
import type { TokenServiceInterface } from "../../server/services/token.ts";
|
|
26
|
+
import type { UserServiceInterface } from "../../server/services/user.ts";
|
|
27
|
+
import type { MemoryUserShape } from "../services.ts";
|
|
28
|
+
/**
|
|
29
|
+
* The point inside a store's {@linkcode TokenServiceInterface.rotate} at which
|
|
30
|
+
* {@link runTokenRotationContractTests} holds a rotation. Wire it into a test
|
|
31
|
+
* build of the store.
|
|
32
|
+
*/
|
|
33
|
+
export interface TokenRotationBarriers {
|
|
34
|
+
/**
|
|
35
|
+
* Await this inside `rotate` after claiming `current` and before persisting
|
|
36
|
+
* `next`, still inside the atomic step: within the transaction and holding
|
|
37
|
+
* the family's lock. When it rejects, fail the rotation as a failed insert
|
|
38
|
+
* would: leave `current` live, persist nothing, and reject or resolve
|
|
39
|
+
* `undefined`.
|
|
40
|
+
*/
|
|
41
|
+
afterClaim(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
/** Services built for each case of {@link runTokenRotationContractTests}. */
|
|
44
|
+
export interface TokenRotationContractServices<C extends ClientInterface, U extends MemoryUserShape, S extends AbstractScope = BasicScope> {
|
|
45
|
+
/** The user service the token service hydrates users from. */
|
|
46
|
+
userService: UserServiceInterface<U>;
|
|
47
|
+
/** The client service the token service hydrates clients from. */
|
|
48
|
+
clientService: ClientServiceInterface<C, U>;
|
|
49
|
+
/** The token service under test. It must implement `rotate`. */
|
|
50
|
+
tokenService: TokenServiceInterface<C, U, S>;
|
|
51
|
+
}
|
|
52
|
+
/** Options for {@link runTokenRotationContractTests}. */
|
|
53
|
+
export interface TokenRotationContractOptions<C extends ClientInterface, U extends MemoryUserShape, S extends AbstractScope = BasicScope> {
|
|
54
|
+
/**
|
|
55
|
+
* Returns fresh services for each case, with `rotate` awaiting
|
|
56
|
+
* `barriers.afterClaim()` between its claim and its insert. A store that
|
|
57
|
+
* sets `rotateAwaitsBarrier: false` ignores `barriers`.
|
|
58
|
+
*/
|
|
59
|
+
makeServices(barriers: TokenRotationBarriers): Promise<TokenRotationContractServices<C, U, S>> | TokenRotationContractServices<C, U, S>;
|
|
60
|
+
/** Persists a user with the given password into the user service under test. */
|
|
61
|
+
addUser(service: UserServiceInterface<U>, user: U, password: string): Promise<void>;
|
|
62
|
+
/** Persists a client into the client service under test. */
|
|
63
|
+
addClient(service: ClientServiceInterface<C, U>, client: C): Promise<void>;
|
|
64
|
+
/** Builds a distinct user fixture for the given sequence number. Defaults to a minimal `{ id, username }` shape. */
|
|
65
|
+
makeUser?(seq: number): U;
|
|
66
|
+
/** Builds a distinct client fixture for the given sequence number. Defaults to a client granting `authorization_code` and `refresh_token`. */
|
|
67
|
+
makeClient?(seq: number): C;
|
|
68
|
+
/**
|
|
69
|
+
* Whether `rotate` awaits `barriers.afterClaim()`. Defaults to `true`: the
|
|
70
|
+
* suite holds a rotation between its claim and its insert while it runs a
|
|
71
|
+
* family revocation, a second rotation, a replayed refresh, or a rotation in
|
|
72
|
+
* another family, and fails the insert there. Set `false` only when
|
|
73
|
+
* `rotate` never awaits between its claim and its insert, as in
|
|
74
|
+
* `MemoryTokenService`; the suite then runs those schedules as overlapping
|
|
75
|
+
* calls, which cannot reach a gap inside `rotate`.
|
|
76
|
+
*/
|
|
77
|
+
rotateAwaitsBarrier?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Milliseconds the suite lets a call run against a held rotation before it
|
|
80
|
+
* releases the rotation; a call that has not settled by then is taken to
|
|
81
|
+
* be waiting on it. Defaults to 100. Raise it for a store whose calls take
|
|
82
|
+
* longer to reach the lock they wait on.
|
|
83
|
+
*/
|
|
84
|
+
blockedAfterMs?: number;
|
|
85
|
+
/** Overrides the name passed to the outer `describe` block. */
|
|
86
|
+
describeName?: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Runs the rotation contract suite: concurrent schedules of
|
|
90
|
+
* {@linkcode TokenServiceInterface.rotate} against itself, against
|
|
91
|
+
* {@linkcode TokenServiceInterface.revokeFamily}, and through the refresh
|
|
92
|
+
* grant. The service must implement `rotate`, `revokeFamily` and
|
|
93
|
+
* `getRevokedRefreshToken`.
|
|
94
|
+
*/
|
|
95
|
+
export declare function runTokenRotationContractTests<C extends ClientInterface, U extends MemoryUserShape, S extends AbstractScope = BasicScope>(options: TokenRotationContractOptions<C, U, S>): void;
|