@udibo/oauth2 0.33.0 → 0.35.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/_cookie-keys.d.ts +17 -0
- package/dist/adapters/hono/bff/_cookie-keys.js +25 -0
- package/dist/adapters/hono/bff/auth-request-store.d.ts +22 -8
- package/dist/adapters/hono/bff/auth-request-store.js +29 -13
- package/dist/adapters/hono/bff/session-store.d.ts +14 -6
- package/dist/adapters/hono/bff/session-store.js +13 -11
- package/dist/cli/idp/server.js +2 -1
- package/dist/server/authorization-server.d.ts +32 -2
- package/dist/server/authorization-server.js +21 -2
- package/dist/server/grants/authorization-code.d.ts +56 -5
- package/dist/server/grants/authorization-code.js +97 -21
- package/dist/server/services/token.d.ts +51 -9
- package/dist/testing/contract/code-exchange.d.ts +100 -0
- package/dist/testing/contract/code-exchange.js +309 -0
- package/dist/testing/contract/mod.d.ts +6 -0
- package/dist/testing/contract/mod.js +6 -0
- package/dist/testing/contract/token.d.ts +8 -0
- package/dist/testing/contract/token.js +27 -0
- package/dist/testing/server.js +3 -2
- package/dist/testing/services.d.ts +23 -5
- package/dist/testing/services.js +40 -8
- package/dist/utils/crypto.d.ts +8 -26
- package/dist/utils/crypto.js +8 -33
- package/package.json +1 -1
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* @module
|
|
5
5
|
*/
|
|
6
6
|
import { snapshotAuthenticationContext } from "../../utils/authentication-context.js";
|
|
7
|
-
import { InvalidClientError, InvalidGrantError, InvalidRequestError, ServerError, } from "../../errors.js";
|
|
7
|
+
import { InvalidClientError, InvalidGrantError, InvalidRequestError, isOAuth2Error, ServerError, } from "../../errors.js";
|
|
8
8
|
import { challengeMethods as defaultChallengeMethods, getChallengeMethod, validateCodeVerifier, } from "../../utils/pkce.js";
|
|
9
9
|
import { authenticateClientCredentials } from "../client-authentication.js";
|
|
10
|
+
import { sha256Hash } from "../utils/hash.js";
|
|
10
11
|
import { AbstractGrant, } from "./grant.js";
|
|
11
12
|
/**
|
|
12
13
|
* The authorization code grant type.
|
|
@@ -37,6 +38,7 @@ export class AuthorizationCodeGrant extends AbstractGrant {
|
|
|
37
38
|
* @see https://datatracker.ietf.org/doc/html/rfc7636#section-4.5
|
|
38
39
|
*/
|
|
39
40
|
requireClientAuthentication;
|
|
41
|
+
#onCodeReuse;
|
|
40
42
|
/** Creates the grant from its {@linkcode AuthorizationCodeGrantOptions}. */
|
|
41
43
|
constructor(options) {
|
|
42
44
|
super({
|
|
@@ -47,6 +49,7 @@ export class AuthorizationCodeGrant extends AbstractGrant {
|
|
|
47
49
|
this.requirePKCE = options.requirePKCE ?? true;
|
|
48
50
|
this.requireClientAuthentication =
|
|
49
51
|
options.requireClientAuthentication ?? true;
|
|
52
|
+
this.#onCodeReuse = options.onCodeReuse;
|
|
50
53
|
}
|
|
51
54
|
/**
|
|
52
55
|
* Extracts client credentials from a request, carrying the PKCE
|
|
@@ -164,11 +167,27 @@ export class AuthorizationCodeGrant extends AbstractGrant {
|
|
|
164
167
|
* Exchanges an authorization code for an access token. If PKCE was used
|
|
165
168
|
* during authorization, the code_verifier must be provided.
|
|
166
169
|
*
|
|
167
|
-
*
|
|
168
|
-
* `redirect_uri`
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
170
|
+
* A failed exchange still consumes the code: one that fails the
|
|
171
|
+
* client-match, verifier or `redirect_uri` check, whose token cannot be
|
|
172
|
+
* generated, or that `exchangeCode` refuses with an OAuth2 error, is claimed
|
|
173
|
+
* before the error is thrown. Any other error from `exchangeCode`, such as a
|
|
174
|
+
* lost database connection, leaves the code claimable. Only the caller whose
|
|
175
|
+
* claim wins receives a token, given an atomic
|
|
176
|
+
* `authorizationCodeService.revoke`.
|
|
177
|
+
*
|
|
178
|
+
* When the token service implements
|
|
179
|
+
* {@linkcode TokenServiceInterface.exchangeCode}, the grant claims the code
|
|
180
|
+
* and saves the token in that one call. Otherwise it claims the code with
|
|
181
|
+
* `authorizationCodeService.revoke` before its checks and saves the token
|
|
182
|
+
* with `tokenService.save` after them; a replay that lands between those
|
|
183
|
+
* two calls finds no token to revoke, so the token saved afterwards stays
|
|
184
|
+
* live.
|
|
185
|
+
*
|
|
186
|
+
* A code presented after it was claimed, whether it was gone when the
|
|
187
|
+
* request read it or a concurrent exchange won the claim, even one that
|
|
188
|
+
* then fails its checks, asks
|
|
189
|
+
* `tokenService.revokeCode` to revoke the tokens issued from it on behalf of
|
|
190
|
+
* the presenting client, fires `onCodeReuse` when it did, and fails with
|
|
172
191
|
* `invalid_grant` (RFC 6819 §4.4.1.1).
|
|
173
192
|
*
|
|
174
193
|
* @throws {InvalidRequestError} When `code` is missing, a required `code_verifier` is absent, or the verifier is malformed.
|
|
@@ -183,17 +202,53 @@ export class AuthorizationCodeGrant extends AbstractGrant {
|
|
|
183
202
|
if (typeof code !== "string") {
|
|
184
203
|
throw new InvalidRequestError("code parameter required");
|
|
185
204
|
}
|
|
186
|
-
// RFC 6819 §4.4.1.1: code replay revokes already-issued tokens and rejects.
|
|
187
|
-
if (await tokenService.revokeCode(code)) {
|
|
188
|
-
throw new InvalidGrantError("code already used");
|
|
189
|
-
}
|
|
190
205
|
const authorizationCode = await authorizationCodeService.get(code);
|
|
191
|
-
if (!authorizationCode
|
|
206
|
+
if (!authorizationCode) {
|
|
207
|
+
throw await this.#refuseClaimedCode(tokenService, code, client, "invalid code");
|
|
208
|
+
}
|
|
209
|
+
if (authorizationCode.expiresAt < new Date()) {
|
|
192
210
|
throw new InvalidGrantError("invalid code");
|
|
193
211
|
}
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
const exchangeCode = tokenService.exchangeCode?.bind(tokenService);
|
|
213
|
+
if (!exchangeCode &&
|
|
214
|
+
!(await authorizationCodeService.revoke(authorizationCode))) {
|
|
215
|
+
throw await this.#refuseClaimedCode(tokenService, code, client);
|
|
216
|
+
}
|
|
217
|
+
let token;
|
|
218
|
+
try {
|
|
219
|
+
await this.#checkExchange(authorizationCode, client, body);
|
|
220
|
+
token = await this.generateToken(client, authorizationCode.user, authorizationCode.scope, tokenService, authorizationCode.authenticationContext);
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
if (exchangeCode) {
|
|
224
|
+
await this.#consumeFailedExchange(authorizationCodeService, tokenService, authorizationCode, client);
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
token.code = code;
|
|
229
|
+
let saved;
|
|
230
|
+
if (exchangeCode) {
|
|
231
|
+
try {
|
|
232
|
+
saved = await exchangeCode(authorizationCode, token);
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (isOAuth2Error(error)) {
|
|
236
|
+
await this.#consumeFailedExchange(authorizationCodeService, tokenService, authorizationCode, client);
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
saved = await tokenService.save(token);
|
|
243
|
+
}
|
|
244
|
+
if (!saved) {
|
|
245
|
+
throw await this.#refuseClaimedCode(tokenService, code, client);
|
|
196
246
|
}
|
|
247
|
+
if (authorizationCode.nonce)
|
|
248
|
+
saved.nonce = authorizationCode.nonce;
|
|
249
|
+
return saved;
|
|
250
|
+
}
|
|
251
|
+
async #checkExchange(authorizationCode, client, body) {
|
|
197
252
|
const codeVerifier = body.get("code_verifier");
|
|
198
253
|
if (typeof codeVerifier === "string") {
|
|
199
254
|
if (!validateCodeVerifier(codeVerifier)) {
|
|
@@ -209,11 +264,11 @@ export class AuthorizationCodeGrant extends AbstractGrant {
|
|
|
209
264
|
else if (this.requirePKCE) {
|
|
210
265
|
throw new InvalidRequestError("PKCE is required for this grant");
|
|
211
266
|
}
|
|
212
|
-
|
|
213
|
-
if (client.id.toString() !== codeClient.id.toString()) {
|
|
267
|
+
if (client.id.toString() !== authorizationCode.client.id.toString()) {
|
|
214
268
|
throw new InvalidClientError("code was issued to another client");
|
|
215
269
|
}
|
|
216
270
|
const redirectUri = body.get("redirect_uri");
|
|
271
|
+
const expectedRedirectUri = authorizationCode.redirectUri;
|
|
217
272
|
if (expectedRedirectUri) {
|
|
218
273
|
if (typeof redirectUri !== "string") {
|
|
219
274
|
throw new InvalidGrantError("redirect_uri parameter required");
|
|
@@ -225,11 +280,32 @@ export class AuthorizationCodeGrant extends AbstractGrant {
|
|
|
225
280
|
else if (redirectUri) {
|
|
226
281
|
throw new InvalidGrantError("did not expect redirect_uri parameter");
|
|
227
282
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
283
|
+
}
|
|
284
|
+
async #consumeFailedExchange(authorizationCodeService, tokenService, authorizationCode, client) {
|
|
285
|
+
try {
|
|
286
|
+
if (!(await authorizationCodeService.revoke(authorizationCode))) {
|
|
287
|
+
await this.#refuseClaimedCode(tokenService, authorizationCode.code, client);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
console.error("[@udibo/oauth2] could not consume the code of a failed exchange:", error instanceof Error ? error.message : error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async #refuseClaimedCode(tokenService, code, client, unrevokedMessage = "code already used") {
|
|
295
|
+
if (!(await tokenService.revokeCode(code, client))) {
|
|
296
|
+
return new InvalidGrantError(unrevokedMessage);
|
|
297
|
+
}
|
|
298
|
+
await this.#notifyCodeReuse({ codeHash: await sha256Hash(code), client });
|
|
299
|
+
return new InvalidGrantError("code already used");
|
|
300
|
+
}
|
|
301
|
+
async #notifyCodeReuse(event) {
|
|
302
|
+
if (!this.#onCodeReuse)
|
|
303
|
+
return;
|
|
304
|
+
try {
|
|
305
|
+
await this.#onCodeReuse(event);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
console.error("[@udibo/oauth2] onCodeReuse hook failed:", error instanceof Error ? error.message : error);
|
|
309
|
+
}
|
|
234
310
|
}
|
|
235
311
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ClientInterface } from "../../models/client.ts";
|
|
2
2
|
import type { AuthenticationContext } from "../../models/authentication.ts";
|
|
3
|
+
import type { AuthorizationCode } from "../../models/authorization-code.ts";
|
|
3
4
|
import type { RefreshToken, Token } from "../../models/token.ts";
|
|
4
5
|
import type { AbstractScope, BasicScope } from "../../models/scope.ts";
|
|
5
6
|
import type { ClientServiceInterface } from "./client.ts";
|
|
@@ -156,16 +157,55 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
|
|
|
156
157
|
*/
|
|
157
158
|
rotate?(current: RefreshToken<Client, User, Scope>, next: RefreshToken<Client, User, Scope>): Promise<RefreshToken<Client, User, Scope> | undefined>;
|
|
158
159
|
/**
|
|
159
|
-
* Revokes all tokens issued with the given authorization code
|
|
160
|
-
*
|
|
160
|
+
* Revokes all tokens issued with the given authorization code, called by the
|
|
161
|
+
* authorization code grant when a code is presented after it was claimed.
|
|
162
|
+
* Returns true if any tokens were revoked, which the grant reports as a code
|
|
163
|
+
* replay (RFC 6819 §4.4.1.1).
|
|
161
164
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
+
* `client` is the client that presented the code. Revoke only when it is the
|
|
166
|
+
* client the code's tokens were issued to; for any other client revoke
|
|
167
|
+
* nothing and resolve `false`. A different confidential client could never
|
|
168
|
+
* have redeemed the code, since that needs the code's own client's
|
|
169
|
+
* credentials, so its replay says nothing about theft. The gate names a
|
|
170
|
+
* public client, or any client when the grant's
|
|
171
|
+
* `requireClientAuthentication` is `false`, by `client_id` alone, so anyone
|
|
172
|
+
* holding such a client's spent code can still present it as that client
|
|
173
|
+
* and revoke. A store that ignores `client` revokes on any client's replay,
|
|
174
|
+
* which lets a client that sees a leaked, spent code end the session it
|
|
175
|
+
* started.
|
|
176
|
+
*
|
|
177
|
+
* With {@linkcode exchangeCode}, make the two linearizable: a call that
|
|
178
|
+
* begins once `exchangeCode` has claimed the code must find and revoke the
|
|
179
|
+
* token it saved. Committing the claim and the insert together, as one
|
|
180
|
+
* database transaction does, meets this.
|
|
165
181
|
*
|
|
166
182
|
* @see https://datatracker.ietf.org/doc/html/rfc6819#section-4.4.1.1
|
|
167
183
|
*/
|
|
168
|
-
revokeCode(code: string): Promise<boolean>;
|
|
184
|
+
revokeCode(code: string, client?: Client): Promise<boolean>;
|
|
185
|
+
/**
|
|
186
|
+
* Exchanges an authorization code in one atomic step: claims
|
|
187
|
+
* `authorizationCode` as `AuthorizationCodeServiceInterface.revoke`
|
|
188
|
+
* would, and persists `token`, the token issued from it, or does neither.
|
|
189
|
+
* Resolves the saved token, or `undefined` with nothing persisted when the
|
|
190
|
+
* code was already claimed or never stored. Concurrent calls for one code
|
|
191
|
+
* persist exactly one token. `token.code` is already set to the code.
|
|
192
|
+
*
|
|
193
|
+
* When implemented, the authorization code grant calls it instead of
|
|
194
|
+
* claiming the code through the authorization code service and saving the
|
|
195
|
+
* token with {@linkcode save}. Without it, the grant claims and saves in two
|
|
196
|
+
* calls, and a replay that arrives between them finds the code claimed but
|
|
197
|
+
* no token to revoke: the replay is refused, and the token the first
|
|
198
|
+
* exchange saves next stays live. With it, a replay finds the token
|
|
199
|
+
* {@linkcode revokeCode} can revoke.
|
|
200
|
+
*
|
|
201
|
+
* After a successful call the code service's `get` must resolve `undefined`
|
|
202
|
+
* for the code, as after `revoke`. A failed insert must leave the code
|
|
203
|
+
* claimable. Store the token so `revokeCode` finds it by the code, and make
|
|
204
|
+
* the two linearizable, as `revokeCode` requires.
|
|
205
|
+
* `runCodeExchangeContractTests` checks it under concurrent callers, with
|
|
206
|
+
* the store's barrier wired between its claim and its insert.
|
|
207
|
+
*/
|
|
208
|
+
exchangeCode?(authorizationCode: AuthorizationCode<Client, User, Scope>, token: Token<Client, User, Scope>): Promise<Token<Client, User, Scope> | undefined>;
|
|
169
209
|
/**
|
|
170
210
|
* Looks up a refresh token that has already been revoked/rotated out. The
|
|
171
211
|
* refresh grant's reuse detection reads it, and so does the revocation
|
|
@@ -188,8 +228,8 @@ export interface TokenServiceInterface<Client extends ClientInterface, User, Sco
|
|
|
188
228
|
* given a token whose tombstone carries {@linkcode RefreshToken.rotatedAt}.
|
|
189
229
|
* Returns true if anything was revoked.
|
|
190
230
|
*
|
|
191
|
-
* `/revoke` ends the family without firing `onTokenReuse
|
|
192
|
-
*
|
|
231
|
+
* `/revoke` ends the family without firing `onTokenReuse`; it reports the
|
|
232
|
+
* revocation to `AuthorizationServerOptions.onRotatedOutRevocation` instead.
|
|
193
233
|
*
|
|
194
234
|
* Without {@linkcode rotate}, keep the family revoked: a successor that a
|
|
195
235
|
* concurrent rotation saves after this call must be stored already revoked,
|
|
@@ -325,6 +365,8 @@ export declare abstract class AbstractTokenService<Client extends ClientInterfac
|
|
|
325
365
|
/**
|
|
326
366
|
* Revokes every token issued from the given authorization code, returning
|
|
327
367
|
* true if any were revoked (signaling code replay per RFC 6819 §4.4.1.1).
|
|
368
|
+
* Revoke nothing when `client` is not the client the tokens were issued to
|
|
369
|
+
* ({@linkcode TokenServiceInterface.revokeCode} explains why).
|
|
328
370
|
*/
|
|
329
|
-
abstract revokeCode(code: string): Promise<boolean>;
|
|
371
|
+
abstract revokeCode(code: string, client?: Client): Promise<boolean>;
|
|
330
372
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract test suite for {@link TokenServiceInterface.exchangeCode} and its
|
|
3
|
+
* linearizability with {@link TokenServiceInterface.revokeCode}.
|
|
4
|
+
*
|
|
5
|
+
* `runTokenServiceContractTests` checks `revokeCode` for one caller. This
|
|
6
|
+
* suite checks the atomic code exchange under concurrent callers: two
|
|
7
|
+
* exchanges of one code, a replay of the code racing its exchange through
|
|
8
|
+
* the authorization code grant, an exchange of an unrelated code, and an
|
|
9
|
+
* exchange whose insert fails. Each case asserts the store's end state, the
|
|
10
|
+
* live tokens and whether the code can still be claimed, not only the value
|
|
11
|
+
* each call resolved.
|
|
12
|
+
*
|
|
13
|
+
* The store under test awaits {@link CodeExchangeBarriers.afterClaim} inside
|
|
14
|
+
* `exchangeCode`, between its claim and its insert, so the suite can hold an
|
|
15
|
+
* exchange there and race the other call against it; a store that never
|
|
16
|
+
* reaches the barrier fails every held case. Only a store whose
|
|
17
|
+
* `exchangeCode` never awaits between its claim and its insert, such as
|
|
18
|
+
* `MemoryTokenService` paired with its code store, opts out with
|
|
19
|
+
* `exchangeAwaitsBarrier: false`; the suite then runs those schedules as
|
|
20
|
+
* overlapping calls.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { ClientInterface } from "../../models/client.ts";
|
|
25
|
+
import type { AbstractScope, BasicScope } from "../../models/scope.ts";
|
|
26
|
+
import type { AuthorizationCodeServiceInterface } from "../../server/services/authorization-code.ts";
|
|
27
|
+
import type { ClientServiceInterface } from "../../server/services/client.ts";
|
|
28
|
+
import type { TokenServiceInterface } from "../../server/services/token.ts";
|
|
29
|
+
import type { UserServiceInterface } from "../../server/services/user.ts";
|
|
30
|
+
import type { MemoryUserShape } from "../services.ts";
|
|
31
|
+
/**
|
|
32
|
+
* The point inside a store's {@linkcode TokenServiceInterface.exchangeCode}
|
|
33
|
+
* at which {@link runCodeExchangeContractTests} holds an exchange. Wire it
|
|
34
|
+
* into a test build of the store.
|
|
35
|
+
*/
|
|
36
|
+
export interface CodeExchangeBarriers {
|
|
37
|
+
/**
|
|
38
|
+
* Await this inside `exchangeCode` after claiming the code and before
|
|
39
|
+
* persisting the token, still inside the atomic step: within the
|
|
40
|
+
* transaction and holding the code's lock. When it rejects, fail the
|
|
41
|
+
* exchange as a failed insert would: leave the code claimable, persist
|
|
42
|
+
* nothing, and reject or resolve `undefined`.
|
|
43
|
+
*/
|
|
44
|
+
afterClaim(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
/** Services built for each case of {@link runCodeExchangeContractTests}. */
|
|
47
|
+
export interface CodeExchangeContractServices<C extends ClientInterface, U extends MemoryUserShape, S extends AbstractScope = BasicScope> {
|
|
48
|
+
/** The user service both stores hydrate users from. */
|
|
49
|
+
userService: UserServiceInterface<U>;
|
|
50
|
+
/** The client service both stores hydrate clients from. */
|
|
51
|
+
clientService: ClientServiceInterface<C, U>;
|
|
52
|
+
/** The code store `exchangeCode` claims codes from. */
|
|
53
|
+
authorizationCodeService: AuthorizationCodeServiceInterface<C, U, S>;
|
|
54
|
+
/** The token service under test. It must implement `exchangeCode`. */
|
|
55
|
+
tokenService: TokenServiceInterface<C, U, S>;
|
|
56
|
+
}
|
|
57
|
+
/** Options for {@link runCodeExchangeContractTests}. */
|
|
58
|
+
export interface CodeExchangeContractOptions<C extends ClientInterface, U extends MemoryUserShape, S extends AbstractScope = BasicScope> {
|
|
59
|
+
/**
|
|
60
|
+
* Returns fresh services for each case, with `exchangeCode` awaiting
|
|
61
|
+
* `barriers.afterClaim()` between its claim and its insert. A store that
|
|
62
|
+
* sets `exchangeAwaitsBarrier: false` ignores `barriers`.
|
|
63
|
+
*/
|
|
64
|
+
makeServices(barriers: CodeExchangeBarriers): Promise<CodeExchangeContractServices<C, U, S>> | CodeExchangeContractServices<C, U, S>;
|
|
65
|
+
/** Persists a user with the given password into the user service under test. */
|
|
66
|
+
addUser(service: UserServiceInterface<U>, user: U, password: string): Promise<void>;
|
|
67
|
+
/** Persists a client into the client service under test. */
|
|
68
|
+
addClient(service: ClientServiceInterface<C, U>, client: C): Promise<void>;
|
|
69
|
+
/** Builds a distinct user fixture for the given sequence number. Defaults to a minimal `{ id, username }` shape. */
|
|
70
|
+
makeUser?(seq: number): U;
|
|
71
|
+
/** Builds a distinct client fixture for the given sequence number. Defaults to a client granting `authorization_code` and `refresh_token`. */
|
|
72
|
+
makeClient?(seq: number): C;
|
|
73
|
+
/**
|
|
74
|
+
* Whether `exchangeCode` awaits `barriers.afterClaim()`. Defaults to
|
|
75
|
+
* `true`: the suite holds an exchange between its claim and its insert
|
|
76
|
+
* while it runs a second exchange, a replay through the grant, or an
|
|
77
|
+
* exchange of another code, and fails the insert there. Set `false` only
|
|
78
|
+
* when `exchangeCode` never awaits between its claim and its insert, as in
|
|
79
|
+
* `MemoryTokenService`; the suite then runs those schedules as overlapping
|
|
80
|
+
* calls, which cannot reach a gap inside `exchangeCode`.
|
|
81
|
+
*/
|
|
82
|
+
exchangeAwaitsBarrier?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Milliseconds the suite lets a call run against a held exchange before it
|
|
85
|
+
* releases the exchange; a call that has not settled by then is taken to be
|
|
86
|
+
* waiting on it. Defaults to 100. Raise it for a store whose calls take
|
|
87
|
+
* longer to reach the lock they wait on.
|
|
88
|
+
*/
|
|
89
|
+
blockedAfterMs?: number;
|
|
90
|
+
/** Overrides the name passed to the outer `describe` block. */
|
|
91
|
+
describeName?: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Runs the code exchange contract suite: concurrent schedules of
|
|
95
|
+
* {@linkcode TokenServiceInterface.exchangeCode} against itself, and against
|
|
96
|
+
* a replay of the code through the authorization code grant, which calls
|
|
97
|
+
* {@linkcode TokenServiceInterface.revokeCode}. The service must implement
|
|
98
|
+
* `exchangeCode`.
|
|
99
|
+
*/
|
|
100
|
+
export declare function runCodeExchangeContractTests<C extends ClientInterface, U extends MemoryUserShape, S extends AbstractScope = BasicScope>(options: CodeExchangeContractOptions<C, U, S>): void;
|