@hyper.authio/testing 0.0.0-stage → 0.7.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.
@@ -0,0 +1,342 @@
1
+ import { JWK } from 'jose';
2
+ import { AuthioClock } from '@hyper.authio/client';
3
+
4
+ /**
5
+ * How the fake encrypts what it issues to a client, as Authio does once the client registered an encryption key.
6
+ */
7
+ interface FakeAuthioClientEncryption {
8
+ /** The client's public encryption key (RSA). */
9
+ key: JWK;
10
+ /** Defaults to `RSA-OAEP-256`. */
11
+ algorithm?: "RSA-OAEP" | "RSA-OAEP-256";
12
+ /** Defaults to `A256CBC-HS512`. */
13
+ encryptionMethod?: "A128CBC-HS256" | "A192CBC-HS384" | "A256CBC-HS512";
14
+ /** Encrypts the client's access tokens (nested JWT). */
15
+ accessTokens?: boolean;
16
+ /** Encrypts the client's ID tokens. Defaults to `true`. */
17
+ idTokens?: boolean;
18
+ /** Encrypts UserInfo answered as a JWT. Defaults to `true`. */
19
+ userInfo?: boolean;
20
+ /** Encrypts back-channel logout tokens. */
21
+ logoutTokens?: boolean;
22
+ }
23
+ /**
24
+ * A client registered in the fake.
25
+ */
26
+ interface FakeAuthioClient {
27
+ clientId: string;
28
+ /** For `client_secret_basic` and `client_secret_post`. */
29
+ clientSecret?: string;
30
+ /** The public JWK `private_key_jwt` assertions are verified with. */
31
+ publicKey?: JWK;
32
+ /** Scopes granted when a request names none. */
33
+ defaultScopes?: string[];
34
+ /** Extra claims put into every access token issued to the client. */
35
+ claims?: Record<string, unknown>;
36
+ /** The client this one's tokens are introspectable by, besides itself (a resource server's introspection client). */
37
+ introspectionClients?: string[];
38
+ /** The redirect URIs the authorization endpoint accepts. Any when absent. */
39
+ redirectUris?: string[];
40
+ /** The post-logout redirect URIs the end-session endpoint accepts. Any when absent. */
41
+ postLogoutRedirectUris?: string[];
42
+ /** Where back-channel logout tokens are posted when a session of the client ends. */
43
+ backchannelLogoutUri?: string;
44
+ /** Encrypts what is issued to the client. */
45
+ encryption?: FakeAuthioClientEncryption;
46
+ }
47
+ /**
48
+ * A person the fake knows, for the password grant, authorization codes and UserInfo.
49
+ */
50
+ interface FakeAuthioUser {
51
+ sub: string;
52
+ username: string;
53
+ password?: string;
54
+ /** Claims put into the person's access tokens: `*_resource_access`, `org_id`, mapper claims… */
55
+ claims?: Record<string, unknown>;
56
+ /** What UserInfo answers besides `sub`. */
57
+ userInfo?: Record<string, unknown>;
58
+ /** The sign-in's methods. Defaults to `["pwd"]`. */
59
+ amr?: string[];
60
+ /** The sign-in's level. Defaults to `1`, or the `acr_values` asked for. */
61
+ acr?: string;
62
+ }
63
+ /**
64
+ * A personal access token the fake accepts with the `personal_access_token` grant.
65
+ */
66
+ interface FakeAuthioPersonalAccessToken {
67
+ /** The `pat_…` value. */
68
+ token: string;
69
+ /** The credential's id, the token's `sub`. */
70
+ sub: string;
71
+ /** The consumer's client, for a consumer's token; the `authio` client of an account otherwise. */
72
+ clientId?: string;
73
+ /** Claims added to the access token. */
74
+ claims?: Record<string, unknown>;
75
+ }
76
+ /**
77
+ * An error the fake answers a request with.
78
+ */
79
+ interface FakeAuthioError {
80
+ status: number;
81
+ body?: Record<string, unknown>;
82
+ headers?: Record<string, string>;
83
+ }
84
+ /**
85
+ * A request the fake received.
86
+ */
87
+ interface FakeAuthioRecordedRequest {
88
+ method: string;
89
+ path: string;
90
+ headers: Record<string, string>;
91
+ form: Record<string, string>;
92
+ /** The query parameters. */
93
+ query: Record<string, string>;
94
+ /** The client the request authenticated as, when it did. */
95
+ clientId?: string;
96
+ }
97
+ /**
98
+ * A back-channel logout token the fake posted to a client.
99
+ */
100
+ interface FakeAuthioBackchannelDelivery {
101
+ clientId: string;
102
+ uri: string;
103
+ logoutToken: string;
104
+ /** The HTTP status the client answered, or 0 when it could not be reached. */
105
+ status: number;
106
+ }
107
+ /**
108
+ * The parameters an authorization ended with, as the fake sent them back.
109
+ */
110
+ interface FakeAuthioAuthorizationOutcome {
111
+ /** `code`, `state` and `iss`, or `error`, `error_description`, `state` and `iss`. */
112
+ parameters: Record<string, string>;
113
+ /** The redirect URI with the parameters, for `response_mode=query`. */
114
+ callbackUrl: string | undefined;
115
+ }
116
+ /**
117
+ * What the device flow answers to successive polls.
118
+ */
119
+ type FakeAuthioDeviceOutcome = "pending" | "slow_down" | "approved" | "denied" | "expired";
120
+ /**
121
+ * Options of the fake.
122
+ */
123
+ interface FakeAuthioOptions {
124
+ /** The realm. Defaults to `fake`. */
125
+ realm?: string;
126
+ /** Seconds tokens live. Defaults to 300. */
127
+ accessTokenLifetime?: number;
128
+ clients?: FakeAuthioClient[];
129
+ users?: FakeAuthioUser[];
130
+ personalAccessTokens?: FakeAuthioPersonalAccessToken[];
131
+ }
132
+ /**
133
+ * Options of a token minted directly.
134
+ */
135
+ interface FakeAuthioTokenOptions {
136
+ sub?: string;
137
+ /** One or several audiences. Defaults to the client id. */
138
+ audience?: string | string[];
139
+ clientId?: string;
140
+ scope?: string;
141
+ /** Seconds. Negative values mint an expired token. */
142
+ expiresIn?: number;
143
+ /** The `typ` claim. Defaults to `Bearer`. */
144
+ type?: string;
145
+ /** Claims added to the payload. */
146
+ claims?: Record<string, unknown>;
147
+ /** Signs with another issuer. */
148
+ issuer?: string;
149
+ /** Signs with another key, so the signature does not verify. */
150
+ untrusted?: boolean;
151
+ /** Encrypts the token (nested JWT) to this public key, as Authio does for a client with an encryption key. */
152
+ encryptTo?: {
153
+ key: JWK;
154
+ algorithm?: "RSA-OAEP" | "RSA-OAEP-256";
155
+ encryptionMethod?: string;
156
+ };
157
+ }
158
+ type Endpoint = "token" | "introspect" | "revoke" | "userinfo" | "device" | "discovery" | "authorize" | "logout";
159
+ /**
160
+ * An in-memory Authio realm served over HTTP on loopback: discovery, JWKS, authorization (PKCE, `state`, `nonce`,
161
+ * `prompt`, `max_age`, `acr_values`, `response_mode=web_message`, RFC 9207 `iss`), token (client credentials,
162
+ * authorization code with ID tokens, refresh, token exchange, password, device code, personal access token),
163
+ * introspection, revocation, UserInfo, device authorization, end session and back-channel logout delivery, with JWE
164
+ * (RSA-OAEP, RSA-OAEP-256) for clients that registered an encryption key.
165
+ */
166
+ declare class FakeAuthio {
167
+ /** Every request received, in order. */
168
+ readonly requests: FakeAuthioRecordedRequest[];
169
+ /** Answers the next requests to an endpoint with these errors, in order, before answering normally. */
170
+ readonly failures: Map<Endpoint, FakeAuthioError[]>;
171
+ /** Every back-channel logout token posted, in order. */
172
+ readonly backchannelDeliveries: FakeAuthioBackchannelDelivery[];
173
+ /** What successive device-code polls answer; the last value repeats. Defaults to `["approved"]`. */
174
+ deviceOutcomes: FakeAuthioDeviceOutcome[];
175
+ /** Overrides the discovery document's issuer, to test the mismatch check. */
176
+ discoveryIssuer: string | undefined;
177
+ /** Answers UserInfo as `application/jwt`, signed by the realm key (and encrypted for a client with a key). */
178
+ userInfoAsJwt: boolean;
179
+ /** Leaves `iss` out of authorization responses, to test the RFC 9207 check. */
180
+ omitAuthorizationIssuer: boolean;
181
+ /** Answers connected-account exchanges. Defaults to granting a token for the provider. */
182
+ connectionResponder: ((form: Record<string, string>) => FakeAuthioError | Record<string, unknown>) | undefined;
183
+ /** The person the authorization endpoint signs in when the request names none with `login_hint`. Defaults to the first user. */
184
+ signInAs: string | undefined;
185
+ private readonly clients;
186
+ private readonly users;
187
+ private readonly personalAccessTokens;
188
+ private readonly codes;
189
+ private readonly refreshTokens;
190
+ private readonly issued;
191
+ private readonly devicePolls;
192
+ private readonly lifetime;
193
+ private session;
194
+ private server;
195
+ private signingKey;
196
+ private verificationKey;
197
+ private untrustedKey;
198
+ private publicJwk;
199
+ private baseUrl;
200
+ readonly realm: string;
201
+ readonly keyId = "fake-key-1";
202
+ private constructor();
203
+ /**
204
+ * Starts the fake on a random loopback port.
205
+ */
206
+ static start(options?: FakeAuthioOptions): Promise<FakeAuthio>;
207
+ /** `http://127.0.0.1:{port}`. */
208
+ get authority(): string;
209
+ /** `{authority}/realms/{realm}`. */
210
+ get issuer(): string;
211
+ /** The realm's public signing key. */
212
+ get jwks(): {
213
+ keys: JWK[];
214
+ };
215
+ /** The session the authorization endpoint keeps (one browser), or `undefined` when nobody is signed in. */
216
+ get currentSession(): {
217
+ sub: string;
218
+ sid: string;
219
+ authTime: number;
220
+ } | undefined;
221
+ /** Registers a client. */
222
+ addClient(client: FakeAuthioClient): void;
223
+ /** Registers a person. */
224
+ addUser(user: FakeAuthioUser): void;
225
+ /** Registers a personal access token. */
226
+ addPersonalAccessToken(token: FakeAuthioPersonalAccessToken): void;
227
+ /** Ends the signed-in session without notifying anyone, as if it expired. */
228
+ endSession(): void;
229
+ /**
230
+ * Registers an authorization code for a person, as the authorization endpoint would after sign-in.
231
+ */
232
+ createAuthorizationCode(options: {
233
+ clientId: string;
234
+ username: string;
235
+ redirectUri: string;
236
+ scope?: string;
237
+ codeChallengeVerifier?: string;
238
+ resource?: string;
239
+ nonce?: string;
240
+ }): string;
241
+ /**
242
+ * Follows an authorization URL as a browser would, signing in `signInAs` (or the `login_hint`), and returns what
243
+ * Authio sent back: the callback URL for `response_mode=query`, the posted message's parameters for `web_message`.
244
+ */
245
+ authorize(authorizationUrl: string): Promise<FakeAuthioAuthorizationOutcome>;
246
+ /**
247
+ * Mints an access token signed by the realm key, recorded so introspection and revocation know it.
248
+ */
249
+ issueAccessToken(options?: FakeAuthioTokenOptions): Promise<string>;
250
+ /**
251
+ * Mints a back-channel logout token for a client, signed by the realm key. `overrides` replaces claims (`undefined`
252
+ * removes one) and `type` the header `typ`, to build invalid ones in tests.
253
+ */
254
+ createLogoutToken(options: {
255
+ clientId: string;
256
+ sub?: string;
257
+ sid?: string;
258
+ type?: string;
259
+ overrides?: Record<string, unknown>;
260
+ }): Promise<string>;
261
+ /**
262
+ * Posts a back-channel logout token to the client's `backchannelLogoutUri`, as Authio does when a session ends.
263
+ *
264
+ * @returns The HTTP status the client answered, or 0 when it could not be reached.
265
+ */
266
+ deliverBackchannelLogout(clientId: string, options?: {
267
+ sub?: string;
268
+ sid?: string;
269
+ }): Promise<number>;
270
+ /** Marks a token revoked, as Authio would after a sign-out: an access token, or a refresh token, which is then forgotten. */
271
+ revokeToken(token: string): void;
272
+ /** Revokes every refresh token of a person, or of one Authio session (`sid`), as Authio does when a session ends. */
273
+ revokeRefreshTokens(options: {
274
+ sub?: string;
275
+ sid?: string;
276
+ }): void;
277
+ /** Forgets the recorded requests. */
278
+ clearRequests(): void;
279
+ /** The recorded requests to one endpoint path suffix, such as `/token`. */
280
+ requestsTo(suffix: string): FakeAuthioRecordedRequest[];
281
+ /** Stops the server. */
282
+ close(): Promise<void>;
283
+ private listen;
284
+ private handle;
285
+ private discovery;
286
+ private authorizeEndpoint;
287
+ private endSessionEndpoint;
288
+ private authenticate;
289
+ private token;
290
+ private tokenAnswer;
291
+ private introspect;
292
+ private revoke;
293
+ private device;
294
+ private userInfo;
295
+ private requireUser;
296
+ }
297
+
298
+ /**
299
+ * A clock that moves only when told to. `sleep` returns at once and moves the clock forward by the time asked, so
300
+ * device polling and cache expiry run instantly and deterministically.
301
+ */
302
+ declare class ManualClock implements AuthioClock {
303
+ /** Every `sleep` asked for, in milliseconds. */
304
+ readonly sleeps: number[];
305
+ private time;
306
+ /**
307
+ * @param start Milliseconds since the epoch. Defaults to the current time.
308
+ */
309
+ constructor(start?: number);
310
+ now(): number;
311
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
312
+ /** Moves the clock forward. */
313
+ advance(ms: number): void;
314
+ /** Sets the clock. */
315
+ set(ms: number): void;
316
+ }
317
+
318
+ /**
319
+ * A webhook delivery as Authio would send it.
320
+ */
321
+ interface FakeWebhookDelivery {
322
+ readonly body: string;
323
+ readonly headers: Record<string, string>;
324
+ readonly event: Record<string, unknown>;
325
+ }
326
+ /**
327
+ * Builds a signed webhook delivery.
328
+ *
329
+ * @param type The event type.
330
+ * @param data The event data.
331
+ * @param secret The webhook secret.
332
+ * @param options `id`, `timestamp` (milliseconds) and extra event members.
333
+ */
334
+ declare function createWebhookDelivery(type: string, data: unknown, secret: string, options?: {
335
+ id?: string;
336
+ timestamp?: number;
337
+ realmId?: string;
338
+ clientId?: string;
339
+ sendEventIdHeader?: boolean;
340
+ }): Promise<FakeWebhookDelivery>;
341
+
342
+ export { FakeAuthio, type FakeAuthioAuthorizationOutcome, type FakeAuthioBackchannelDelivery, type FakeAuthioClient, type FakeAuthioClientEncryption, type FakeAuthioDeviceOutcome, type FakeAuthioError, type FakeAuthioOptions, type FakeAuthioPersonalAccessToken, type FakeAuthioRecordedRequest, type FakeAuthioTokenOptions, type FakeAuthioUser, type FakeWebhookDelivery, ManualClock, createWebhookDelivery };