@hyper.authio/testing 0.0.0-stage → 0.1.1
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 +44 -2
- package/dist/index.cjs +453 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +215 -0
- package/dist/index.d.ts +215 -0
- package/dist/index.js +449 -0
- package/dist/index.js.map +1 -0
- package/package.json +50 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { createServer } from 'http';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
3
|
+
import { SignJWT, decodeJwt, generateKeyPair, exportJWK, importJWK, jwtVerify } from 'jose';
|
|
4
|
+
import { AuthioWebhookHeaders, computeWebhookSignatureHeader } from '@hyper.authio/client';
|
|
5
|
+
|
|
6
|
+
// src/fake-authio.ts
|
|
7
|
+
var FakeAuthio = class _FakeAuthio {
|
|
8
|
+
/** Every request received, in order. */
|
|
9
|
+
requests = [];
|
|
10
|
+
/** Answers the next requests to an endpoint with these errors, in order, before answering normally. */
|
|
11
|
+
failures = /* @__PURE__ */ new Map();
|
|
12
|
+
/** What successive device-code polls answer; the last value repeats. Defaults to `["approved"]`. */
|
|
13
|
+
deviceOutcomes = ["approved"];
|
|
14
|
+
/** Overrides the discovery document's issuer, to test the mismatch check. */
|
|
15
|
+
discoveryIssuer;
|
|
16
|
+
/** Answers UserInfo as `application/jwt`, signed by the realm key. */
|
|
17
|
+
userInfoAsJwt = false;
|
|
18
|
+
/** Answers connected-account exchanges. Defaults to granting a token for the provider. */
|
|
19
|
+
connectionResponder;
|
|
20
|
+
clients = /* @__PURE__ */ new Map();
|
|
21
|
+
users = /* @__PURE__ */ new Map();
|
|
22
|
+
codes = /* @__PURE__ */ new Map();
|
|
23
|
+
refreshTokens = /* @__PURE__ */ new Map();
|
|
24
|
+
issued = /* @__PURE__ */ new Map();
|
|
25
|
+
devicePolls = /* @__PURE__ */ new Map();
|
|
26
|
+
lifetime;
|
|
27
|
+
server;
|
|
28
|
+
signingKey;
|
|
29
|
+
untrustedKey;
|
|
30
|
+
publicJwk;
|
|
31
|
+
baseUrl;
|
|
32
|
+
realm;
|
|
33
|
+
keyId = "fake-key-1";
|
|
34
|
+
constructor(options) {
|
|
35
|
+
this.realm = options.realm ?? "fake";
|
|
36
|
+
this.lifetime = options.accessTokenLifetime ?? 300;
|
|
37
|
+
for (const client of options.clients ?? []) this.clients.set(client.clientId, client);
|
|
38
|
+
for (const user of options.users ?? []) this.users.set(user.username, user);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Starts the fake on a random loopback port.
|
|
42
|
+
*/
|
|
43
|
+
static async start(options = {}) {
|
|
44
|
+
const fake = new _FakeAuthio(options);
|
|
45
|
+
await fake.listen();
|
|
46
|
+
return fake;
|
|
47
|
+
}
|
|
48
|
+
/** `http://127.0.0.1:{port}`. */
|
|
49
|
+
get authority() {
|
|
50
|
+
return this.baseUrl;
|
|
51
|
+
}
|
|
52
|
+
/** `{authority}/realms/{realm}`. */
|
|
53
|
+
get issuer() {
|
|
54
|
+
return `${this.baseUrl}/realms/${this.realm}`;
|
|
55
|
+
}
|
|
56
|
+
/** The realm's public signing key. */
|
|
57
|
+
get jwks() {
|
|
58
|
+
return { keys: [this.publicJwk] };
|
|
59
|
+
}
|
|
60
|
+
/** Registers a client. */
|
|
61
|
+
addClient(client) {
|
|
62
|
+
this.clients.set(client.clientId, client);
|
|
63
|
+
}
|
|
64
|
+
/** Registers a person. */
|
|
65
|
+
addUser(user) {
|
|
66
|
+
this.users.set(user.username, user);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Registers an authorization code for a person, as the authorization endpoint would after sign-in.
|
|
70
|
+
*/
|
|
71
|
+
createAuthorizationCode(options) {
|
|
72
|
+
const user = this.requireUser(options.username);
|
|
73
|
+
const code = randomUUID();
|
|
74
|
+
this.codes.set(code, {
|
|
75
|
+
clientId: options.clientId,
|
|
76
|
+
sub: user.sub,
|
|
77
|
+
redirectUri: options.redirectUri,
|
|
78
|
+
scope: options.scope,
|
|
79
|
+
codeVerifier: options.codeChallengeVerifier,
|
|
80
|
+
resource: options.resource
|
|
81
|
+
});
|
|
82
|
+
return code;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Mints an access token signed by the realm key, recorded so introspection and revocation know it.
|
|
86
|
+
*/
|
|
87
|
+
async issueAccessToken(options = {}) {
|
|
88
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
89
|
+
const clientId = options.clientId ?? "fake-client";
|
|
90
|
+
const jti = randomUUID();
|
|
91
|
+
const payload = {
|
|
92
|
+
typ: options.type ?? "Bearer",
|
|
93
|
+
azp: clientId,
|
|
94
|
+
client_id: clientId,
|
|
95
|
+
...options.scope ? { scope: options.scope } : {},
|
|
96
|
+
...options.sub ? { sub: options.sub } : {},
|
|
97
|
+
...options.claims
|
|
98
|
+
};
|
|
99
|
+
const token = await new SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId, typ: "JWT" }).setIssuer(options.issuer ?? this.issuer).setAudience(options.audience ?? clientId).setIssuedAt(now).setNotBefore(now).setExpirationTime(now + (options.expiresIn ?? this.lifetime)).setJti(jti).sign(options.untrusted ? this.untrustedKey : this.signingKey);
|
|
100
|
+
this.issued.set(token, { claims: decodeJwt(token), clientId, revoked: false });
|
|
101
|
+
return token;
|
|
102
|
+
}
|
|
103
|
+
/** Marks a token revoked, as Authio would after a sign-out. */
|
|
104
|
+
revokeToken(token) {
|
|
105
|
+
const issued = this.issued.get(token);
|
|
106
|
+
if (issued) issued.revoked = true;
|
|
107
|
+
}
|
|
108
|
+
/** Forgets the recorded requests. */
|
|
109
|
+
clearRequests() {
|
|
110
|
+
this.requests.length = 0;
|
|
111
|
+
}
|
|
112
|
+
/** The recorded requests to one endpoint path suffix, such as `/token`. */
|
|
113
|
+
requestsTo(suffix) {
|
|
114
|
+
return this.requests.filter((request) => request.path.endsWith(suffix));
|
|
115
|
+
}
|
|
116
|
+
/** Stops the server. */
|
|
117
|
+
close() {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
this.server.closeAllConnections?.();
|
|
120
|
+
this.server.close((error) => error ? reject(error) : resolve());
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async listen() {
|
|
124
|
+
const pair = await generateKeyPair("RS256", { extractable: true });
|
|
125
|
+
const untrusted = await generateKeyPair("RS256");
|
|
126
|
+
this.signingKey = pair.privateKey;
|
|
127
|
+
this.untrustedKey = untrusted.privateKey;
|
|
128
|
+
this.publicJwk = { ...await exportJWK(pair.publicKey), kid: this.keyId, alg: "RS256", use: "sig" };
|
|
129
|
+
this.server = createServer((request, response) => {
|
|
130
|
+
this.handle(request, response).catch((error) => {
|
|
131
|
+
send(response, { status: 500, body: { error: "server_error", error_description: String(error) } });
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
await new Promise((resolve) => this.server.listen(0, "127.0.0.1", resolve));
|
|
135
|
+
this.baseUrl = `http://127.0.0.1:${this.server.address().port}`;
|
|
136
|
+
}
|
|
137
|
+
async handle(request, response) {
|
|
138
|
+
const url = new URL(request.url ?? "/", this.baseUrl);
|
|
139
|
+
const body = await readBody(request);
|
|
140
|
+
const form = Object.fromEntries(new URLSearchParams(request.headers["content-type"]?.includes("form") ? body : ""));
|
|
141
|
+
const headers = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name, Array.isArray(value) ? value.join(", ") : value ?? ""]));
|
|
142
|
+
const recorded = { method: request.method ?? "GET", path: url.pathname, headers, form };
|
|
143
|
+
this.requests.push(recorded);
|
|
144
|
+
const prefix = `/realms/${this.realm}`;
|
|
145
|
+
const path = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : url.pathname;
|
|
146
|
+
const endpoint = endpointOf(path);
|
|
147
|
+
const failure = endpoint ? this.failures.get(endpoint)?.shift() : void 0;
|
|
148
|
+
if (failure) return send(response, failure);
|
|
149
|
+
switch (path) {
|
|
150
|
+
case "/.well-known/openid-configuration":
|
|
151
|
+
return send(response, { status: 200, body: this.discovery() });
|
|
152
|
+
case "/protocol/openid-connect/certs":
|
|
153
|
+
return send(response, { status: 200, body: this.jwks });
|
|
154
|
+
case "/protocol/openid-connect/token":
|
|
155
|
+
return send(response, await this.token(recorded, form));
|
|
156
|
+
case "/protocol/openid-connect/token/introspect":
|
|
157
|
+
return send(response, await this.introspect(recorded, form));
|
|
158
|
+
case "/protocol/openid-connect/revoke":
|
|
159
|
+
return send(response, await this.revoke(recorded, form));
|
|
160
|
+
case "/protocol/openid-connect/userinfo":
|
|
161
|
+
return this.userInfo(headers, response);
|
|
162
|
+
case "/protocol/openid-connect/auth/device":
|
|
163
|
+
return send(response, await this.device(recorded, form));
|
|
164
|
+
default:
|
|
165
|
+
return send(response, { status: 404, body: { error: "not_found" } });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
discovery() {
|
|
169
|
+
const base = `${this.issuer}/protocol/openid-connect`;
|
|
170
|
+
return {
|
|
171
|
+
issuer: this.discoveryIssuer ?? this.issuer,
|
|
172
|
+
authorization_endpoint: `${base}/auth`,
|
|
173
|
+
token_endpoint: `${base}/token`,
|
|
174
|
+
introspection_endpoint: `${base}/token/introspect`,
|
|
175
|
+
revocation_endpoint: `${base}/revoke`,
|
|
176
|
+
userinfo_endpoint: `${base}/userinfo`,
|
|
177
|
+
end_session_endpoint: `${base}/logout`,
|
|
178
|
+
device_authorization_endpoint: `${base}/auth/device`,
|
|
179
|
+
jwks_uri: `${base}/certs`,
|
|
180
|
+
grant_types_supported: ["authorization_code", "refresh_token", "client_credentials", "password", "urn:ietf:params:oauth:grant-type:device_code", "urn:ietf:params:oauth:grant-type:token-exchange"],
|
|
181
|
+
token_endpoint_auth_methods_supported: ["client_secret_basic", "client_secret_post", "private_key_jwt", "none"],
|
|
182
|
+
code_challenge_methods_supported: ["S256"],
|
|
183
|
+
authorization_response_iss_parameter_supported: true
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
async authenticate(recorded, form) {
|
|
187
|
+
const invalid = (description) => ({ status: 401, body: { error: "invalid_client", error_description: description } });
|
|
188
|
+
const authorization = recorded.headers.authorization;
|
|
189
|
+
let client;
|
|
190
|
+
if (authorization?.startsWith("Basic ")) {
|
|
191
|
+
const [id, secret] = Buffer.from(authorization.slice(6), "base64").toString("utf8").split(":").map(decodeURIComponent);
|
|
192
|
+
client = this.clients.get(id ?? "");
|
|
193
|
+
if (!client || !client.clientSecret || client.clientSecret !== secret) return invalid("The client credentials are wrong.");
|
|
194
|
+
} else if (form.client_assertion) {
|
|
195
|
+
client = this.clients.get(form.client_id ?? "");
|
|
196
|
+
if (!client?.publicKey) return invalid("The client has no registered key.");
|
|
197
|
+
if (form.client_assertion_type !== "urn:ietf:params:oauth:client-assertion-type:jwt-bearer") return invalid("Wrong assertion type.");
|
|
198
|
+
try {
|
|
199
|
+
const key = await importJWK(client.publicKey, client.publicKey.alg);
|
|
200
|
+
const { payload } = await jwtVerify(form.client_assertion, key, { issuer: client.clientId, subject: client.clientId, audience: this.issuer });
|
|
201
|
+
if (!payload.jti) return invalid("The assertion has no jti.");
|
|
202
|
+
} catch (error) {
|
|
203
|
+
return invalid(`The assertion does not verify: ${error.message}`);
|
|
204
|
+
}
|
|
205
|
+
} else if (form.client_secret !== void 0) {
|
|
206
|
+
client = this.clients.get(form.client_id ?? "");
|
|
207
|
+
if (!client || client.clientSecret !== form.client_secret) return invalid("The client credentials are wrong.");
|
|
208
|
+
} else {
|
|
209
|
+
client = this.clients.get(form.client_id ?? "");
|
|
210
|
+
if (!client || client.clientSecret || client.publicKey) return invalid("The client must authenticate.");
|
|
211
|
+
}
|
|
212
|
+
recorded.clientId = client.clientId;
|
|
213
|
+
return client;
|
|
214
|
+
}
|
|
215
|
+
async token(recorded, form) {
|
|
216
|
+
const client = await this.authenticate(recorded, form);
|
|
217
|
+
if ("status" in client) return client;
|
|
218
|
+
const scope = form.scope ?? client.defaultScopes?.join(" ");
|
|
219
|
+
const grant = (error, description) => ({ status: 400, body: { error, error_description: description } });
|
|
220
|
+
switch (form.grant_type) {
|
|
221
|
+
case "client_credentials":
|
|
222
|
+
return this.tokenAnswer(client, { sub: client.clientId, scope, audience: form.resource, claims: client.claims });
|
|
223
|
+
case "password": {
|
|
224
|
+
const user = this.users.get(form.username ?? "");
|
|
225
|
+
if (!user || user.password !== form.password) return grant("invalid_grant", "Wrong username or password.");
|
|
226
|
+
return this.tokenAnswer(client, { sub: user.sub, scope, claims: { ...client.claims, ...user.claims, preferred_username: user.username } }, user);
|
|
227
|
+
}
|
|
228
|
+
case "authorization_code": {
|
|
229
|
+
const code = this.codes.get(form.code ?? "");
|
|
230
|
+
if (!code || code.clientId !== client.clientId || code.redirectUri !== form.redirect_uri) return grant("invalid_grant", "The code is not valid.");
|
|
231
|
+
if (code.codeVerifier && code.codeVerifier !== form.code_verifier) return grant("invalid_grant", "PKCE verification failed.");
|
|
232
|
+
this.codes.delete(form.code);
|
|
233
|
+
const user = [...this.users.values()].find((candidate) => candidate.sub === code.sub);
|
|
234
|
+
return this.tokenAnswer(
|
|
235
|
+
client,
|
|
236
|
+
{ sub: user.sub, scope: code.scope, audience: form.resource ?? code.resource, claims: { ...client.claims, ...user.claims, preferred_username: user.username } },
|
|
237
|
+
user
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
case "refresh_token": {
|
|
241
|
+
const refresh = this.refreshTokens.get(form.refresh_token ?? "");
|
|
242
|
+
if (!refresh || refresh.clientId !== client.clientId) return grant("invalid_grant", "The refresh token is not valid.");
|
|
243
|
+
this.refreshTokens.delete(form.refresh_token);
|
|
244
|
+
const user = [...this.users.values()].find((candidate) => candidate.sub === refresh.sub);
|
|
245
|
+
return this.tokenAnswer(
|
|
246
|
+
client,
|
|
247
|
+
{ sub: refresh.sub, scope: form.scope ?? refresh.scope, claims: { ...client.claims, ...user?.claims } },
|
|
248
|
+
user
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
case "urn:ietf:params:oauth:grant-type:token-exchange": {
|
|
252
|
+
const subject = this.issued.get(form.subject_token ?? "");
|
|
253
|
+
if (!subject || subject.revoked) return grant("invalid_grant", "The subject token is not valid.");
|
|
254
|
+
if (form.requested_token_type === "urn:authio:params:oauth:token-type:connection-access-token") {
|
|
255
|
+
const answer = this.connectionResponder?.(form) ?? {
|
|
256
|
+
access_token: `provider-token-${form.connection_provider}`,
|
|
257
|
+
issued_token_type: form.requested_token_type,
|
|
258
|
+
token_type: "Bearer",
|
|
259
|
+
expires_in: 3600,
|
|
260
|
+
provider: form.connection_provider
|
|
261
|
+
};
|
|
262
|
+
return "status" in answer && typeof answer.status === "number" ? answer : { status: 200, body: answer };
|
|
263
|
+
}
|
|
264
|
+
const { iss: _issuer, aud: _audience, exp: _expiry, iat: _issuedAt, nbf: _notBefore, jti: _jti, ...claims } = subject.claims;
|
|
265
|
+
return this.tokenAnswer(client, {
|
|
266
|
+
sub: subject.claims.sub,
|
|
267
|
+
scope: form.scope ?? subject.claims.scope,
|
|
268
|
+
audience: form.resource,
|
|
269
|
+
claims: { ...claims, act: { client_id: client.clientId, sub: client.clientId } }
|
|
270
|
+
}, void 0, form.requested_token_type);
|
|
271
|
+
}
|
|
272
|
+
case "urn:ietf:params:oauth:grant-type:device_code": {
|
|
273
|
+
const polls = this.devicePolls.get(form.device_code ?? "");
|
|
274
|
+
if (polls === void 0) return grant("invalid_grant", "Unknown device code.");
|
|
275
|
+
this.devicePolls.set(form.device_code, polls + 1);
|
|
276
|
+
const outcome = this.deviceOutcomes[Math.min(polls, this.deviceOutcomes.length - 1)] ?? "approved";
|
|
277
|
+
switch (outcome) {
|
|
278
|
+
case "pending":
|
|
279
|
+
return grant("authorization_pending", "The user has not approved yet.");
|
|
280
|
+
case "slow_down":
|
|
281
|
+
return grant("slow_down", "Poll less often.");
|
|
282
|
+
case "denied":
|
|
283
|
+
return grant("access_denied", "The user denied the request.");
|
|
284
|
+
case "expired":
|
|
285
|
+
return grant("expired_token", "The device code expired.");
|
|
286
|
+
default:
|
|
287
|
+
return this.tokenAnswer(client, { sub: "device-user", scope, claims: client.claims });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
default:
|
|
291
|
+
return grant("unsupported_grant_type", `The grant ${form.grant_type} is not supported.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async tokenAnswer(client, options, user, issuedTokenType) {
|
|
295
|
+
const accessToken = await this.issueAccessToken({
|
|
296
|
+
clientId: client.clientId,
|
|
297
|
+
sub: options.sub,
|
|
298
|
+
scope: options.scope,
|
|
299
|
+
audience: options.audience ?? client.clientId,
|
|
300
|
+
claims: options.claims
|
|
301
|
+
});
|
|
302
|
+
const body = {
|
|
303
|
+
access_token: accessToken,
|
|
304
|
+
token_type: "Bearer",
|
|
305
|
+
expires_in: this.lifetime,
|
|
306
|
+
...options.scope ? { scope: options.scope } : {},
|
|
307
|
+
...issuedTokenType ? { issued_token_type: issuedTokenType } : {}
|
|
308
|
+
};
|
|
309
|
+
if (user) {
|
|
310
|
+
const refreshToken = randomUUID();
|
|
311
|
+
this.refreshTokens.set(refreshToken, { clientId: client.clientId, sub: user.sub, scope: options.scope });
|
|
312
|
+
body.refresh_token = refreshToken;
|
|
313
|
+
}
|
|
314
|
+
return { status: 200, body };
|
|
315
|
+
}
|
|
316
|
+
async introspect(recorded, form) {
|
|
317
|
+
const client = await this.authenticate(recorded, form);
|
|
318
|
+
if ("status" in client) return client;
|
|
319
|
+
const issued = this.issued.get(form.token ?? "");
|
|
320
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
321
|
+
if (!issued || issued.revoked || (issued.claims.exp ?? 0) <= now) return { status: 200, body: { active: false } };
|
|
322
|
+
const audiences = [issued.claims.aud ?? []].flat();
|
|
323
|
+
const allowed = issued.clientId === client.clientId && audiences.includes(client.clientId) ? true : audiences.some((audience) => this.clients.get(audience)?.introspectionClients?.includes(client.clientId) || audience === client.clientId);
|
|
324
|
+
if (!allowed) return { status: 200, body: { active: false } };
|
|
325
|
+
const { typ, ...claims } = issued.claims;
|
|
326
|
+
return { status: 200, body: { active: true, token_type: typ ?? "Bearer", username: claims.preferred_username, ...claims } };
|
|
327
|
+
}
|
|
328
|
+
async revoke(recorded, form) {
|
|
329
|
+
const client = await this.authenticate(recorded, form);
|
|
330
|
+
if ("status" in client) return client;
|
|
331
|
+
this.revokeToken(form.token ?? "");
|
|
332
|
+
this.refreshTokens.delete(form.token ?? "");
|
|
333
|
+
return { status: 200 };
|
|
334
|
+
}
|
|
335
|
+
async device(recorded, form) {
|
|
336
|
+
const client = await this.authenticate(recorded, form);
|
|
337
|
+
if ("status" in client) return client;
|
|
338
|
+
const deviceCode = randomUUID();
|
|
339
|
+
this.devicePolls.set(deviceCode, 0);
|
|
340
|
+
return {
|
|
341
|
+
status: 200,
|
|
342
|
+
body: {
|
|
343
|
+
device_code: deviceCode,
|
|
344
|
+
user_code: "WDJB-MJHT",
|
|
345
|
+
verification_uri: `${this.issuer}/device`,
|
|
346
|
+
verification_uri_complete: `${this.issuer}/device?user_code=WDJB-MJHT`,
|
|
347
|
+
expires_in: 600,
|
|
348
|
+
interval: 5
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
async userInfo(headers, response) {
|
|
353
|
+
const token = headers.authorization?.replace(/^Bearer\s+/i, "");
|
|
354
|
+
const issued = token ? this.issued.get(token) : void 0;
|
|
355
|
+
if (!issued || issued.revoked)
|
|
356
|
+
return send(response, {
|
|
357
|
+
status: 401,
|
|
358
|
+
headers: { "www-authenticate": 'Bearer error="invalid_token", error_description="The access token is not valid."' }
|
|
359
|
+
});
|
|
360
|
+
const user = [...this.users.values()].find((candidate) => candidate.sub === issued.claims.sub);
|
|
361
|
+
const claims = { sub: issued.claims.sub, ...user?.userInfo };
|
|
362
|
+
if (!this.userInfoAsJwt) return send(response, { status: 200, body: claims });
|
|
363
|
+
const jwt = await new SignJWT(claims).setProtectedHeader({ alg: "RS256", kid: this.keyId }).setIssuer(this.issuer).setAudience(issued.clientId).setIssuedAt().sign(this.signingKey);
|
|
364
|
+
response.writeHead(200, { "content-type": "application/jwt" });
|
|
365
|
+
response.end(jwt);
|
|
366
|
+
}
|
|
367
|
+
requireUser(username) {
|
|
368
|
+
const user = this.users.get(username);
|
|
369
|
+
if (!user) throw new Error(`The fake has no user ${username}.`);
|
|
370
|
+
return user;
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
function endpointOf(path) {
|
|
374
|
+
if (path.endsWith("/token")) return "token";
|
|
375
|
+
if (path.endsWith("/introspect")) return "introspect";
|
|
376
|
+
if (path.endsWith("/revoke")) return "revoke";
|
|
377
|
+
if (path.endsWith("/userinfo")) return "userinfo";
|
|
378
|
+
if (path.endsWith("/auth/device")) return "device";
|
|
379
|
+
if (path.endsWith("/openid-configuration")) return "discovery";
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
function send(response, answer) {
|
|
383
|
+
const headers = { ...answer.headers };
|
|
384
|
+
if (answer.body !== void 0) headers["content-type"] ??= "application/json";
|
|
385
|
+
response.writeHead(answer.status, headers);
|
|
386
|
+
response.end(answer.body === void 0 ? void 0 : JSON.stringify(answer.body));
|
|
387
|
+
}
|
|
388
|
+
function readBody(request) {
|
|
389
|
+
return new Promise((resolve, reject) => {
|
|
390
|
+
const chunks = [];
|
|
391
|
+
request.on("data", (chunk) => chunks.push(chunk));
|
|
392
|
+
request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
393
|
+
request.on("error", reject);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/manual-clock.ts
|
|
398
|
+
var ManualClock = class {
|
|
399
|
+
/** Every `sleep` asked for, in milliseconds. */
|
|
400
|
+
sleeps = [];
|
|
401
|
+
time;
|
|
402
|
+
/**
|
|
403
|
+
* @param start Milliseconds since the epoch. Defaults to the current time.
|
|
404
|
+
*/
|
|
405
|
+
constructor(start = Date.now()) {
|
|
406
|
+
this.time = start;
|
|
407
|
+
}
|
|
408
|
+
now() {
|
|
409
|
+
return this.time;
|
|
410
|
+
}
|
|
411
|
+
sleep(ms, signal) {
|
|
412
|
+
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
413
|
+
this.sleeps.push(ms);
|
|
414
|
+
this.time += ms;
|
|
415
|
+
return Promise.resolve();
|
|
416
|
+
}
|
|
417
|
+
/** Moves the clock forward. */
|
|
418
|
+
advance(ms) {
|
|
419
|
+
this.time += ms;
|
|
420
|
+
}
|
|
421
|
+
/** Sets the clock. */
|
|
422
|
+
set(ms) {
|
|
423
|
+
this.time = ms;
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
async function createWebhookDelivery(type, data, secret, options = {}) {
|
|
427
|
+
const timestamp = options.timestamp ?? Date.now();
|
|
428
|
+
const id = options.id ?? randomUUID();
|
|
429
|
+
const event = {
|
|
430
|
+
id,
|
|
431
|
+
type,
|
|
432
|
+
created_at: Math.floor(timestamp / 1e3),
|
|
433
|
+
...options.realmId ? { realm_id: options.realmId } : {},
|
|
434
|
+
...options.clientId ? { client_id: options.clientId } : {},
|
|
435
|
+
data
|
|
436
|
+
};
|
|
437
|
+
const body = JSON.stringify(event);
|
|
438
|
+
const headers = {
|
|
439
|
+
"content-type": "application/json",
|
|
440
|
+
[AuthioWebhookHeaders.Signature]: await computeWebhookSignatureHeader(body, secret, timestamp),
|
|
441
|
+
[AuthioWebhookHeaders.Event]: type
|
|
442
|
+
};
|
|
443
|
+
if (options.sendEventIdHeader ?? true) headers[AuthioWebhookHeaders.EventId] = id;
|
|
444
|
+
return { body, headers, event };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export { FakeAuthio, ManualClock, createWebhookDelivery };
|
|
448
|
+
//# sourceMappingURL=index.js.map
|
|
449
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/fake-authio.ts","../src/manual-clock.ts","../src/webhook-delivery.ts"],"names":["randomUUID"],"mappings":"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","file":"index.js","sourcesContent":["import { createServer, type IncomingMessage, type Server, type ServerResponse } from \"node:http\";\nimport type { AddressInfo } from \"node:net\";\nimport { randomUUID } from \"node:crypto\";\nimport { decodeJwt, exportJWK, generateKeyPair, importJWK, jwtVerify, SignJWT, type CryptoKey, type JWK, type JWTPayload } from \"jose\";\n\n/**\n * A client registered in the fake.\n */\nexport interface FakeAuthioClient {\n clientId: string;\n /** For `client_secret_basic` and `client_secret_post`. */\n clientSecret?: string;\n /** The public JWK `private_key_jwt` assertions are verified with. */\n publicKey?: JWK;\n /** Scopes granted when a request names none. */\n defaultScopes?: string[];\n /** Extra claims put into every access token issued to the client. */\n claims?: Record<string, unknown>;\n /** The client this one's tokens are introspectable by, besides itself (a resource server's introspection client). */\n introspectionClients?: string[];\n}\n\n/**\n * A person the fake knows, for the password grant, authorization codes and UserInfo.\n */\nexport interface FakeAuthioUser {\n sub: string;\n username: string;\n password?: string;\n /** Claims put into the person's access tokens: `*_resource_access`, `org_id`, mapper claims… */\n claims?: Record<string, unknown>;\n /** What UserInfo answers besides `sub`. */\n userInfo?: Record<string, unknown>;\n}\n\n/**\n * An error the fake answers a request with.\n */\nexport interface FakeAuthioError {\n status: number;\n body?: Record<string, unknown>;\n headers?: Record<string, string>;\n}\n\n/**\n * A request the fake received.\n */\nexport interface FakeAuthioRecordedRequest {\n method: string;\n path: string;\n headers: Record<string, string>;\n form: Record<string, string>;\n /** The client the request authenticated as, when it did. */\n clientId?: string;\n}\n\n/**\n * What the device flow answers to successive polls.\n */\nexport type FakeAuthioDeviceOutcome = \"pending\" | \"slow_down\" | \"approved\" | \"denied\" | \"expired\";\n\n/**\n * Options of the fake.\n */\nexport interface FakeAuthioOptions {\n /** The realm. Defaults to `fake`. */\n realm?: string;\n /** Seconds tokens live. Defaults to 300. */\n accessTokenLifetime?: number;\n clients?: FakeAuthioClient[];\n users?: FakeAuthioUser[];\n}\n\n/**\n * Options of a token minted directly.\n */\nexport interface FakeAuthioTokenOptions {\n sub?: string;\n /** One or several audiences. Defaults to the client id. */\n audience?: string | string[];\n clientId?: string;\n scope?: string;\n /** Seconds. Negative values mint an expired token. */\n expiresIn?: number;\n /** The `typ` claim. Defaults to `Bearer`. */\n type?: string;\n /** Claims added to the payload. */\n claims?: Record<string, unknown>;\n /** Signs with another issuer. */\n issuer?: string;\n /** Signs with another key, so the signature does not verify. */\n untrusted?: boolean;\n}\n\ninterface IssuedToken {\n claims: JWTPayload;\n clientId: string;\n revoked: boolean;\n}\n\n/**\n * An in-memory Authio realm served over HTTP on loopback: discovery, JWKS, token (client credentials,\n * authorization code, refresh, token exchange, password, device code), introspection, revocation, UserInfo and\n * device authorization.\n */\nexport class FakeAuthio {\n /** Every request received, in order. */\n readonly requests: FakeAuthioRecordedRequest[] = [];\n /** Answers the next requests to an endpoint with these errors, in order, before answering normally. */\n readonly failures = new Map<\"token\" | \"introspect\" | \"revoke\" | \"userinfo\" | \"device\" | \"discovery\", FakeAuthioError[]>();\n /** What successive device-code polls answer; the last value repeats. Defaults to `[\"approved\"]`. */\n deviceOutcomes: FakeAuthioDeviceOutcome[] = [\"approved\"];\n /** Overrides the discovery document's issuer, to test the mismatch check. */\n discoveryIssuer: string | undefined;\n /** Answers UserInfo as `application/jwt`, signed by the realm key. */\n userInfoAsJwt = false;\n /** Answers connected-account exchanges. Defaults to granting a token for the provider. */\n connectionResponder: ((form: Record<string, string>) => FakeAuthioError | Record<string, unknown>) | undefined;\n\n private readonly clients = new Map<string, FakeAuthioClient>();\n private readonly users = new Map<string, FakeAuthioUser>();\n private readonly codes = new Map<string, { clientId: string; sub: string; redirectUri: string; scope?: string; codeVerifier?: string; resource?: string }>();\n private readonly refreshTokens = new Map<string, { clientId: string; sub: string; scope?: string }>();\n private readonly issued = new Map<string, IssuedToken>();\n private readonly devicePolls = new Map<string, number>();\n private readonly lifetime: number;\n private server!: Server;\n private signingKey!: CryptoKey;\n private untrustedKey!: CryptoKey;\n private publicJwk!: JWK;\n private baseUrl!: string;\n readonly realm: string;\n readonly keyId = \"fake-key-1\";\n\n private constructor(options: FakeAuthioOptions) {\n this.realm = options.realm ?? \"fake\";\n this.lifetime = options.accessTokenLifetime ?? 300;\n\n for (const client of options.clients ?? []) this.clients.set(client.clientId, client);\n for (const user of options.users ?? []) this.users.set(user.username, user);\n }\n\n /**\n * Starts the fake on a random loopback port.\n */\n static async start(options: FakeAuthioOptions = {}): Promise<FakeAuthio> {\n const fake = new FakeAuthio(options);\n\n await fake.listen();\n\n return fake;\n }\n\n /** `http://127.0.0.1:{port}`. */\n get authority(): string {\n return this.baseUrl;\n }\n\n /** `{authority}/realms/{realm}`. */\n get issuer(): string {\n return `${this.baseUrl}/realms/${this.realm}`;\n }\n\n /** The realm's public signing key. */\n get jwks(): { keys: JWK[] } {\n return { keys: [this.publicJwk] };\n }\n\n /** Registers a client. */\n addClient(client: FakeAuthioClient): void {\n this.clients.set(client.clientId, client);\n }\n\n /** Registers a person. */\n addUser(user: FakeAuthioUser): void {\n this.users.set(user.username, user);\n }\n\n /**\n * Registers an authorization code for a person, as the authorization endpoint would after sign-in.\n */\n createAuthorizationCode(options: { clientId: string; username: string; redirectUri: string; scope?: string; codeChallengeVerifier?: string; resource?: string }): string {\n const user = this.requireUser(options.username);\n const code = randomUUID();\n\n this.codes.set(code, {\n clientId: options.clientId,\n sub: user.sub,\n redirectUri: options.redirectUri,\n scope: options.scope,\n codeVerifier: options.codeChallengeVerifier,\n resource: options.resource,\n });\n\n return code;\n }\n\n /**\n * Mints an access token signed by the realm key, recorded so introspection and revocation know it.\n */\n async issueAccessToken(options: FakeAuthioTokenOptions = {}): Promise<string> {\n const now = Math.floor(Date.now() / 1000);\n const clientId = options.clientId ?? \"fake-client\";\n const jti = randomUUID();\n const payload: JWTPayload = {\n typ: options.type ?? \"Bearer\",\n azp: clientId,\n client_id: clientId,\n ...(options.scope ? { scope: options.scope } : {}),\n ...(options.sub ? { sub: options.sub } : {}),\n ...options.claims,\n };\n const token = await new SignJWT(payload)\n .setProtectedHeader({ alg: \"RS256\", kid: this.keyId, typ: \"JWT\" })\n .setIssuer(options.issuer ?? this.issuer)\n .setAudience(options.audience ?? clientId)\n .setIssuedAt(now)\n .setNotBefore(now)\n .setExpirationTime(now + (options.expiresIn ?? this.lifetime))\n .setJti(jti)\n .sign(options.untrusted ? this.untrustedKey : this.signingKey);\n\n this.issued.set(token, { claims: decodeJwt(token), clientId, revoked: false });\n\n return token;\n }\n\n /** Marks a token revoked, as Authio would after a sign-out. */\n revokeToken(token: string): void {\n const issued = this.issued.get(token);\n\n if (issued) issued.revoked = true;\n }\n\n /** Forgets the recorded requests. */\n clearRequests(): void {\n this.requests.length = 0;\n }\n\n /** The recorded requests to one endpoint path suffix, such as `/token`. */\n requestsTo(suffix: string): FakeAuthioRecordedRequest[] {\n return this.requests.filter((request) => request.path.endsWith(suffix));\n }\n\n /** Stops the server. */\n close(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server.closeAllConnections?.();\n this.server.close((error) => (error ? reject(error) : resolve()));\n });\n }\n\n private async listen(): Promise<void> {\n const pair = await generateKeyPair(\"RS256\", { extractable: true });\n const untrusted = await generateKeyPair(\"RS256\");\n\n this.signingKey = pair.privateKey;\n this.untrustedKey = untrusted.privateKey;\n this.publicJwk = { ...(await exportJWK(pair.publicKey)), kid: this.keyId, alg: \"RS256\", use: \"sig\" };\n this.server = createServer((request, response) => {\n this.handle(request, response).catch((error: unknown) => {\n send(response, { status: 500, body: { error: \"server_error\", error_description: String(error) } });\n });\n });\n\n await new Promise<void>((resolve) => this.server.listen(0, \"127.0.0.1\", resolve));\n\n this.baseUrl = `http://127.0.0.1:${(this.server.address() as AddressInfo).port}`;\n }\n\n private async handle(request: IncomingMessage, response: ServerResponse): Promise<void> {\n const url = new URL(request.url ?? \"/\", this.baseUrl);\n const body = await readBody(request);\n const form = Object.fromEntries(new URLSearchParams(request.headers[\"content-type\"]?.includes(\"form\") ? body : \"\"));\n const headers = Object.fromEntries(Object.entries(request.headers).map(([name, value]) => [name, Array.isArray(value) ? value.join(\", \") : (value ?? \"\")]));\n const recorded: FakeAuthioRecordedRequest = { method: request.method ?? \"GET\", path: url.pathname, headers, form };\n\n this.requests.push(recorded);\n\n const prefix = `/realms/${this.realm}`;\n const path = url.pathname.startsWith(prefix) ? url.pathname.slice(prefix.length) : url.pathname;\n const endpoint = endpointOf(path);\n const failure = endpoint ? this.failures.get(endpoint)?.shift() : undefined;\n\n if (failure) return send(response, failure);\n\n switch (path) {\n case \"/.well-known/openid-configuration\":\n return send(response, { status: 200, body: this.discovery() });\n case \"/protocol/openid-connect/certs\":\n return send(response, { status: 200, body: this.jwks });\n case \"/protocol/openid-connect/token\":\n return send(response, await this.token(recorded, form));\n case \"/protocol/openid-connect/token/introspect\":\n return send(response, await this.introspect(recorded, form));\n case \"/protocol/openid-connect/revoke\":\n return send(response, await this.revoke(recorded, form));\n case \"/protocol/openid-connect/userinfo\":\n return this.userInfo(headers, response);\n case \"/protocol/openid-connect/auth/device\":\n return send(response, await this.device(recorded, form));\n default:\n return send(response, { status: 404, body: { error: \"not_found\" } });\n }\n }\n\n private discovery(): Record<string, unknown> {\n const base = `${this.issuer}/protocol/openid-connect`;\n\n return {\n issuer: this.discoveryIssuer ?? this.issuer,\n authorization_endpoint: `${base}/auth`,\n token_endpoint: `${base}/token`,\n introspection_endpoint: `${base}/token/introspect`,\n revocation_endpoint: `${base}/revoke`,\n userinfo_endpoint: `${base}/userinfo`,\n end_session_endpoint: `${base}/logout`,\n device_authorization_endpoint: `${base}/auth/device`,\n jwks_uri: `${base}/certs`,\n grant_types_supported: [\"authorization_code\", \"refresh_token\", \"client_credentials\", \"password\", \"urn:ietf:params:oauth:grant-type:device_code\", \"urn:ietf:params:oauth:grant-type:token-exchange\"],\n token_endpoint_auth_methods_supported: [\"client_secret_basic\", \"client_secret_post\", \"private_key_jwt\", \"none\"],\n code_challenge_methods_supported: [\"S256\"],\n authorization_response_iss_parameter_supported: true,\n };\n }\n\n private async authenticate(recorded: FakeAuthioRecordedRequest, form: Record<string, string>): Promise<FakeAuthioClient | FakeAuthioError> {\n const invalid = (description: string): FakeAuthioError => ({ status: 401, body: { error: \"invalid_client\", error_description: description } });\n const authorization = recorded.headers.authorization;\n let client: FakeAuthioClient | undefined;\n\n if (authorization?.startsWith(\"Basic \")) {\n const [id, secret] = Buffer.from(authorization.slice(6), \"base64\").toString(\"utf8\").split(\":\").map(decodeURIComponent);\n\n client = this.clients.get(id ?? \"\");\n\n if (!client || !client.clientSecret || client.clientSecret !== secret) return invalid(\"The client credentials are wrong.\");\n } else if (form.client_assertion) {\n client = this.clients.get(form.client_id ?? \"\");\n\n if (!client?.publicKey) return invalid(\"The client has no registered key.\");\n if (form.client_assertion_type !== \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\") return invalid(\"Wrong assertion type.\");\n\n try {\n const key = await importJWK(client.publicKey, client.publicKey.alg);\n const { payload } = await jwtVerify(form.client_assertion, key, { issuer: client.clientId, subject: client.clientId, audience: this.issuer });\n\n if (!payload.jti) return invalid(\"The assertion has no jti.\");\n } catch (error) {\n return invalid(`The assertion does not verify: ${(error as Error).message}`);\n }\n } else if (form.client_secret !== undefined) {\n client = this.clients.get(form.client_id ?? \"\");\n\n if (!client || client.clientSecret !== form.client_secret) return invalid(\"The client credentials are wrong.\");\n } else {\n client = this.clients.get(form.client_id ?? \"\");\n\n if (!client || client.clientSecret || client.publicKey) return invalid(\"The client must authenticate.\");\n }\n\n recorded.clientId = client.clientId;\n\n return client;\n }\n\n private async token(recorded: FakeAuthioRecordedRequest, form: Record<string, string>): Promise<FakeAuthioError> {\n const client = await this.authenticate(recorded, form);\n\n if (\"status\" in client) return client;\n\n const scope = form.scope ?? client.defaultScopes?.join(\" \");\n const grant = (error: string, description: string): FakeAuthioError => ({ status: 400, body: { error, error_description: description } });\n\n switch (form.grant_type) {\n case \"client_credentials\":\n return this.tokenAnswer(client, { sub: client.clientId, scope, audience: form.resource, claims: client.claims });\n case \"password\": {\n const user = this.users.get(form.username ?? \"\");\n\n if (!user || user.password !== form.password) return grant(\"invalid_grant\", \"Wrong username or password.\");\n\n return this.tokenAnswer(client, { sub: user.sub, scope, claims: { ...client.claims, ...user.claims, preferred_username: user.username } }, user);\n }\n case \"authorization_code\": {\n const code = this.codes.get(form.code ?? \"\");\n\n if (!code || code.clientId !== client.clientId || code.redirectUri !== form.redirect_uri) return grant(\"invalid_grant\", \"The code is not valid.\");\n if (code.codeVerifier && code.codeVerifier !== form.code_verifier) return grant(\"invalid_grant\", \"PKCE verification failed.\");\n\n this.codes.delete(form.code!);\n\n const user = [...this.users.values()].find((candidate) => candidate.sub === code.sub)!;\n\n return this.tokenAnswer(\n client,\n { sub: user.sub, scope: code.scope, audience: form.resource ?? code.resource, claims: { ...client.claims, ...user.claims, preferred_username: user.username } },\n user,\n );\n }\n case \"refresh_token\": {\n const refresh = this.refreshTokens.get(form.refresh_token ?? \"\");\n\n if (!refresh || refresh.clientId !== client.clientId) return grant(\"invalid_grant\", \"The refresh token is not valid.\");\n\n this.refreshTokens.delete(form.refresh_token!);\n\n const user = [...this.users.values()].find((candidate) => candidate.sub === refresh.sub);\n\n return this.tokenAnswer(\n client,\n { sub: refresh.sub, scope: form.scope ?? refresh.scope, claims: { ...client.claims, ...user?.claims } },\n user,\n );\n }\n case \"urn:ietf:params:oauth:grant-type:token-exchange\": {\n const subject = this.issued.get(form.subject_token ?? \"\");\n\n if (!subject || subject.revoked) return grant(\"invalid_grant\", \"The subject token is not valid.\");\n\n if (form.requested_token_type === \"urn:authio:params:oauth:token-type:connection-access-token\") {\n const answer = this.connectionResponder?.(form) ?? {\n access_token: `provider-token-${form.connection_provider}`,\n issued_token_type: form.requested_token_type,\n token_type: \"Bearer\",\n expires_in: 3600,\n provider: form.connection_provider,\n };\n\n return \"status\" in answer && typeof answer.status === \"number\" ? (answer as FakeAuthioError) : { status: 200, body: answer as Record<string, unknown> };\n }\n\n const { iss: _issuer, aud: _audience, exp: _expiry, iat: _issuedAt, nbf: _notBefore, jti: _jti, ...claims } = subject.claims;\n\n return this.tokenAnswer(client, {\n sub: subject.claims.sub,\n scope: form.scope ?? (subject.claims.scope as string | undefined),\n audience: form.resource,\n claims: { ...claims, act: { client_id: client.clientId, sub: client.clientId } },\n }, undefined, form.requested_token_type);\n }\n case \"urn:ietf:params:oauth:grant-type:device_code\": {\n const polls = this.devicePolls.get(form.device_code ?? \"\");\n\n if (polls === undefined) return grant(\"invalid_grant\", \"Unknown device code.\");\n\n this.devicePolls.set(form.device_code!, polls + 1);\n\n const outcome = this.deviceOutcomes[Math.min(polls, this.deviceOutcomes.length - 1)] ?? \"approved\";\n\n switch (outcome) {\n case \"pending\":\n return grant(\"authorization_pending\", \"The user has not approved yet.\");\n case \"slow_down\":\n return grant(\"slow_down\", \"Poll less often.\");\n case \"denied\":\n return grant(\"access_denied\", \"The user denied the request.\");\n case \"expired\":\n return grant(\"expired_token\", \"The device code expired.\");\n default:\n return this.tokenAnswer(client, { sub: \"device-user\", scope, claims: client.claims });\n }\n }\n default:\n return grant(\"unsupported_grant_type\", `The grant ${form.grant_type} is not supported.`);\n }\n }\n\n private async tokenAnswer(\n client: FakeAuthioClient,\n options: { sub?: string; scope?: string; audience?: string; claims?: Record<string, unknown> },\n user?: FakeAuthioUser,\n issuedTokenType?: string,\n ): Promise<FakeAuthioError> {\n const accessToken = await this.issueAccessToken({\n clientId: client.clientId,\n sub: options.sub,\n scope: options.scope,\n audience: options.audience ?? client.clientId,\n claims: options.claims,\n });\n const body: Record<string, unknown> = {\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: this.lifetime,\n ...(options.scope ? { scope: options.scope } : {}),\n ...(issuedTokenType ? { issued_token_type: issuedTokenType } : {}),\n };\n\n if (user) {\n const refreshToken = randomUUID();\n\n this.refreshTokens.set(refreshToken, { clientId: client.clientId, sub: user.sub, scope: options.scope });\n body.refresh_token = refreshToken;\n }\n\n return { status: 200, body };\n }\n\n private async introspect(recorded: FakeAuthioRecordedRequest, form: Record<string, string>): Promise<FakeAuthioError> {\n const client = await this.authenticate(recorded, form);\n\n if (\"status\" in client) return client;\n\n const issued = this.issued.get(form.token ?? \"\");\n const now = Math.floor(Date.now() / 1000);\n\n if (!issued || issued.revoked || (issued.claims.exp ?? 0) <= now) return { status: 200, body: { active: false } };\n\n const audiences = [issued.claims.aud ?? []].flat();\n const allowed =\n issued.clientId === client.clientId && audiences.includes(client.clientId)\n ? true\n : audiences.some((audience) => this.clients.get(audience)?.introspectionClients?.includes(client.clientId) || audience === client.clientId);\n\n if (!allowed) return { status: 200, body: { active: false } };\n\n const { typ, ...claims } = issued.claims;\n\n return { status: 200, body: { active: true, token_type: typ ?? \"Bearer\", username: claims.preferred_username, ...claims } };\n }\n\n private async revoke(recorded: FakeAuthioRecordedRequest, form: Record<string, string>): Promise<FakeAuthioError> {\n const client = await this.authenticate(recorded, form);\n\n if (\"status\" in client) return client;\n\n this.revokeToken(form.token ?? \"\");\n this.refreshTokens.delete(form.token ?? \"\");\n\n return { status: 200 };\n }\n\n private async device(recorded: FakeAuthioRecordedRequest, form: Record<string, string>): Promise<FakeAuthioError> {\n const client = await this.authenticate(recorded, form);\n\n if (\"status\" in client) return client;\n\n const deviceCode = randomUUID();\n\n this.devicePolls.set(deviceCode, 0);\n\n return {\n status: 200,\n body: {\n device_code: deviceCode,\n user_code: \"WDJB-MJHT\",\n verification_uri: `${this.issuer}/device`,\n verification_uri_complete: `${this.issuer}/device?user_code=WDJB-MJHT`,\n expires_in: 600,\n interval: 5,\n },\n };\n }\n\n private async userInfo(headers: Record<string, string>, response: ServerResponse): Promise<void> {\n const token = headers.authorization?.replace(/^Bearer\\s+/i, \"\");\n const issued = token ? this.issued.get(token) : undefined;\n\n if (!issued || issued.revoked)\n return send(response, {\n status: 401,\n headers: { \"www-authenticate\": 'Bearer error=\"invalid_token\", error_description=\"The access token is not valid.\"' },\n });\n\n const user = [...this.users.values()].find((candidate) => candidate.sub === issued.claims.sub);\n const claims = { sub: issued.claims.sub, ...user?.userInfo };\n\n if (!this.userInfoAsJwt) return send(response, { status: 200, body: claims });\n\n const jwt = await new SignJWT(claims)\n .setProtectedHeader({ alg: \"RS256\", kid: this.keyId })\n .setIssuer(this.issuer)\n .setAudience(issued.clientId)\n .setIssuedAt()\n .sign(this.signingKey);\n\n response.writeHead(200, { \"content-type\": \"application/jwt\" });\n response.end(jwt);\n }\n\n private requireUser(username: string): FakeAuthioUser {\n const user = this.users.get(username);\n\n if (!user) throw new Error(`The fake has no user ${username}.`);\n\n return user;\n }\n}\n\nfunction endpointOf(path: string): \"token\" | \"introspect\" | \"revoke\" | \"userinfo\" | \"device\" | \"discovery\" | undefined {\n if (path.endsWith(\"/token\")) return \"token\";\n if (path.endsWith(\"/introspect\")) return \"introspect\";\n if (path.endsWith(\"/revoke\")) return \"revoke\";\n if (path.endsWith(\"/userinfo\")) return \"userinfo\";\n if (path.endsWith(\"/auth/device\")) return \"device\";\n if (path.endsWith(\"/openid-configuration\")) return \"discovery\";\n return undefined;\n}\n\nfunction send(response: ServerResponse, answer: FakeAuthioError): void {\n const headers: Record<string, string> = { ...answer.headers };\n\n if (answer.body !== undefined) headers[\"content-type\"] ??= \"application/json\";\n\n response.writeHead(answer.status, headers);\n response.end(answer.body === undefined ? undefined : JSON.stringify(answer.body));\n}\n\nfunction readBody(request: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n\n request.on(\"data\", (chunk: Buffer) => chunks.push(chunk));\n request.on(\"end\", () => resolve(Buffer.concat(chunks).toString(\"utf8\")));\n request.on(\"error\", reject);\n });\n}\n","import type { AuthioClock } from \"@hyper.authio/client\";\n\n/**\n * A clock that moves only when told to. `sleep` returns at once and moves the clock forward by the time asked, so\n * device polling and cache expiry run instantly and deterministically.\n */\nexport class ManualClock implements AuthioClock {\n /** Every `sleep` asked for, in milliseconds. */\n readonly sleeps: number[] = [];\n private time: number;\n\n /**\n * @param start Milliseconds since the epoch. Defaults to the current time.\n */\n constructor(start: number = Date.now()) {\n this.time = start;\n }\n\n now(): number {\n return this.time;\n }\n\n sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) return Promise.reject(signal.reason);\n\n this.sleeps.push(ms);\n this.time += ms;\n\n return Promise.resolve();\n }\n\n /** Moves the clock forward. */\n advance(ms: number): void {\n this.time += ms;\n }\n\n /** Sets the clock. */\n set(ms: number): void {\n this.time = ms;\n }\n}\n","import { randomUUID } from \"node:crypto\";\nimport { AuthioWebhookHeaders, computeWebhookSignatureHeader } from \"@hyper.authio/client\";\n\n/**\n * A webhook delivery as Authio would send it.\n */\nexport interface FakeWebhookDelivery {\n readonly body: string;\n readonly headers: Record<string, string>;\n readonly event: Record<string, unknown>;\n}\n\n/**\n * Builds a signed webhook delivery.\n *\n * @param type The event type.\n * @param data The event data.\n * @param secret The webhook secret.\n * @param options `id`, `timestamp` (milliseconds) and extra event members.\n */\nexport async function createWebhookDelivery(\n type: string,\n data: unknown,\n secret: string,\n options: { id?: string; timestamp?: number; realmId?: string; clientId?: string; sendEventIdHeader?: boolean } = {},\n): Promise<FakeWebhookDelivery> {\n const timestamp = options.timestamp ?? Date.now();\n const id = options.id ?? randomUUID();\n const event = {\n id,\n type,\n created_at: Math.floor(timestamp / 1000),\n ...(options.realmId ? { realm_id: options.realmId } : {}),\n ...(options.clientId ? { client_id: options.clientId } : {}),\n data,\n };\n const body = JSON.stringify(event);\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n [AuthioWebhookHeaders.Signature]: await computeWebhookSignatureHeader(body, secret, timestamp),\n [AuthioWebhookHeaders.Event]: type,\n };\n\n if (options.sendEventIdHeader ?? true) headers[AuthioWebhookHeaders.EventId] = id;\n\n return { body, headers, event };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyper.authio/testing",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "An in-memory Authio realm for tests: discovery, JWKS, every grant, introspection, revocation, UserInfo, signed webhooks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"main": "./dist/index.cjs",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"default": "./dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"require": {
|
|
18
|
+
"types": "./dist/index.d.cts",
|
|
19
|
+
"default": "./dist/index.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20.19"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@hyper.authio/client": "0.1.1",
|
|
36
|
+
"jose": "^6.1.0"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"authio",
|
|
40
|
+
"testing",
|
|
41
|
+
"oauth",
|
|
42
|
+
"oidc"
|
|
43
|
+
],
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/LeonardoFerreira1209/authio-node.git",
|
|
47
|
+
"directory": "packages/testing"
|
|
48
|
+
},
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|