@cboxdk/id-js 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -1
- package/dist/{chunk-Y6BPPCM4.js → chunk-3SRL7SIB.js} +92 -11
- package/dist/chunk-3SRL7SIB.js.map +1 -0
- package/dist/{client-ksHj7ooh.d.cts → client-DkZSRmnO.d.cts} +50 -3
- package/dist/{client-ksHj7ooh.d.ts → client-DkZSRmnO.d.ts} +50 -3
- package/dist/index.cjs +375 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +241 -3
- package/dist/index.d.ts +241 -3
- package/dist/index.js +281 -14
- package/dist/index.js.map +1 -1
- package/dist/nextjs.cjs +91 -12
- package/dist/nextjs.cjs.map +1 -1
- package/dist/nextjs.d.cts +15 -4
- package/dist/nextjs.d.ts +15 -4
- package/dist/nextjs.js +14 -5
- package/dist/nextjs.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-Y6BPPCM4.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
var jose = require('jose');
|
|
4
4
|
|
|
5
|
-
// src/client.ts
|
|
6
|
-
|
|
7
5
|
// src/errors.ts
|
|
8
6
|
var CboxIdError = class extends Error {
|
|
9
7
|
constructor(message) {
|
|
@@ -17,6 +15,268 @@ var InvalidStateError = class extends CboxIdError {
|
|
|
17
15
|
};
|
|
18
16
|
var AuthenticationError = class extends CboxIdError {
|
|
19
17
|
};
|
|
18
|
+
var FrontendApiError = class extends CboxIdError {
|
|
19
|
+
constructor(message, code, status, retryAfter) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.status = status;
|
|
23
|
+
this.retryAfter = retryAfter;
|
|
24
|
+
}
|
|
25
|
+
code;
|
|
26
|
+
status;
|
|
27
|
+
retryAfter;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// src/legacy.ts
|
|
31
|
+
function createLegacyVerifier(options) {
|
|
32
|
+
if (!options.secret || options.secret.length < 32) {
|
|
33
|
+
throw new ConfigurationError(
|
|
34
|
+
"A legacy verifier secret must be at least 32 characters; it is the only thing proving a request came from Cbox ID."
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const tolerance = options.toleranceSeconds ?? 300;
|
|
38
|
+
return async function handle(request) {
|
|
39
|
+
const raw = await request.text();
|
|
40
|
+
if (!await isSignedByCboxId(raw, request.headers.get("X-Cbox-Signature"), options.secret, tolerance)) {
|
|
41
|
+
return json({ error: "unauthorized" }, 401);
|
|
42
|
+
}
|
|
43
|
+
let payload;
|
|
44
|
+
try {
|
|
45
|
+
payload = JSON.parse(raw);
|
|
46
|
+
} catch {
|
|
47
|
+
return json({ error: "invalid_request" }, 400);
|
|
48
|
+
}
|
|
49
|
+
const { email, password } = payload;
|
|
50
|
+
if (typeof email !== "string" || email === "") {
|
|
51
|
+
return json({ error: "invalid_request" }, 400);
|
|
52
|
+
}
|
|
53
|
+
if (typeof password !== "string") {
|
|
54
|
+
return json({ user: null }, 200);
|
|
55
|
+
}
|
|
56
|
+
let user;
|
|
57
|
+
try {
|
|
58
|
+
user = await options.verify(email, password);
|
|
59
|
+
} catch {
|
|
60
|
+
return json({ error: "unavailable" }, 503);
|
|
61
|
+
}
|
|
62
|
+
if (!user) {
|
|
63
|
+
return json({ user: null }, 200);
|
|
64
|
+
}
|
|
65
|
+
return json(
|
|
66
|
+
{
|
|
67
|
+
email: user.email,
|
|
68
|
+
name: user.name ?? null,
|
|
69
|
+
email_verified: user.emailVerified === true,
|
|
70
|
+
...user.passwordHash ? { password_hash: user.passwordHash } : {}
|
|
71
|
+
},
|
|
72
|
+
200
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
async function isSignedByCboxId(body, header, secret, toleranceSeconds) {
|
|
77
|
+
if (!header) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const parts = {};
|
|
81
|
+
for (const piece of header.split(",")) {
|
|
82
|
+
const [key2, value] = piece.split("=", 2);
|
|
83
|
+
if (key2 && value) {
|
|
84
|
+
parts[key2.trim()] = value.trim();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const timestamp = parts["t"] ?? "";
|
|
88
|
+
const signature = parts["v1"] ?? "";
|
|
89
|
+
if (!/^\d+$/.test(timestamp) || signature === "") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if (Math.abs(Math.floor(Date.now() / 1e3) - Number(timestamp)) > toleranceSeconds) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
const key = await crypto.subtle.importKey(
|
|
96
|
+
"raw",
|
|
97
|
+
new TextEncoder().encode(secret),
|
|
98
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
99
|
+
false,
|
|
100
|
+
["sign"]
|
|
101
|
+
);
|
|
102
|
+
const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${timestamp}.${body}`));
|
|
103
|
+
const expected = [...new Uint8Array(mac)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
104
|
+
return timingSafeEqual(expected, signature);
|
|
105
|
+
}
|
|
106
|
+
function timingSafeEqual(a, b) {
|
|
107
|
+
if (a.length !== b.length) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
let diff = 0;
|
|
111
|
+
for (let i = 0; i < a.length; i++) {
|
|
112
|
+
diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
113
|
+
}
|
|
114
|
+
return diff === 0;
|
|
115
|
+
}
|
|
116
|
+
function json(body, status) {
|
|
117
|
+
return new Response(JSON.stringify(body), {
|
|
118
|
+
status,
|
|
119
|
+
headers: { "Content-Type": "application/json" }
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// src/frontend.ts
|
|
124
|
+
var CboxIdFrontend = class {
|
|
125
|
+
issuer;
|
|
126
|
+
publishableKey;
|
|
127
|
+
timeoutMs;
|
|
128
|
+
retries;
|
|
129
|
+
fetchImpl;
|
|
130
|
+
/**
|
|
131
|
+
* The config document, and the in-flight promise for it.
|
|
132
|
+
*
|
|
133
|
+
* Deduplicated rather than merely cached: a page with a sign-in form, a user button and
|
|
134
|
+
* an organization switcher mounts three components in the same tick, and three parallel
|
|
135
|
+
* fetches of the same immutable document is a waste the SDK should absorb rather than
|
|
136
|
+
* ask every consumer to.
|
|
137
|
+
*/
|
|
138
|
+
configPromise = null;
|
|
139
|
+
constructor(options) {
|
|
140
|
+
if (!options.issuer) {
|
|
141
|
+
throw new ConfigurationError("issuer is required.");
|
|
142
|
+
}
|
|
143
|
+
if (!options.publishableKey?.startsWith("pk_")) {
|
|
144
|
+
throw new ConfigurationError(
|
|
145
|
+
"publishableKey must be a pk_test_\u2026 or pk_live_\u2026 key. Client secrets and API keys must never be put in a browser."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
this.issuer = options.issuer.replace(/\/$/, "");
|
|
149
|
+
this.publishableKey = options.publishableKey;
|
|
150
|
+
this.timeoutMs = options.timeoutMs ?? 1e4;
|
|
151
|
+
this.retries = Math.max(0, options.retries ?? 2);
|
|
152
|
+
this.fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
153
|
+
}
|
|
154
|
+
/** Whether this key drives real sign-ins. Handy for a "test mode" badge in your own UI. */
|
|
155
|
+
get isLive() {
|
|
156
|
+
return this.publishableKey.startsWith("pk_live_");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The public sign-in configuration.
|
|
160
|
+
*
|
|
161
|
+
* Fetched once per client instance. The document is small and changes when somebody
|
|
162
|
+
* edits it in the console, so a long-lived page picks changes up on its next full load
|
|
163
|
+
* rather than mid-session — which is the right trade for something that decides layout.
|
|
164
|
+
*/
|
|
165
|
+
async config() {
|
|
166
|
+
this.configPromise ??= this.get("/frontend/v1/config", {}, isFrontendConfig).catch((error) => {
|
|
167
|
+
this.configPromise = null;
|
|
168
|
+
throw error;
|
|
169
|
+
});
|
|
170
|
+
return this.configPromise;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Who is signed in, given an access token your app already holds.
|
|
174
|
+
*
|
|
175
|
+
* Returns `{ user: null }` rather than throwing when nobody is signed in. That is a
|
|
176
|
+
* state, not an error: a user button renders on pages nobody has signed in on, and
|
|
177
|
+
* making callers treat a rejection as a state is how flash-of-signed-out bugs happen.
|
|
178
|
+
*
|
|
179
|
+
* The publishable key grants nothing here — the token is the entire authority.
|
|
180
|
+
*/
|
|
181
|
+
async session(accessToken) {
|
|
182
|
+
if (!accessToken) {
|
|
183
|
+
return { user: null };
|
|
184
|
+
}
|
|
185
|
+
return this.get(
|
|
186
|
+
"/frontend/v1/session",
|
|
187
|
+
{ Authorization: `Bearer ${accessToken}` },
|
|
188
|
+
isFrontendSession
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
async get(path, extraHeaders = {}, validate) {
|
|
192
|
+
let lastError;
|
|
193
|
+
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
194
|
+
if (attempt > 0) {
|
|
195
|
+
await new Promise((resolve) => setTimeout(resolve, 150 * attempt));
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
return await this.attempt(path, extraHeaders, validate);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
lastError = error;
|
|
201
|
+
if (!(error instanceof FrontendApiError) || error.code !== "unavailable") {
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
throw lastError;
|
|
207
|
+
}
|
|
208
|
+
async attempt(path, extraHeaders, validate) {
|
|
209
|
+
const controller = new AbortController();
|
|
210
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
211
|
+
let response;
|
|
212
|
+
try {
|
|
213
|
+
response = await this.fetchImpl(`${this.issuer}${path}`, {
|
|
214
|
+
headers: {
|
|
215
|
+
// A header, never a query string: a query string puts the key in server logs,
|
|
216
|
+
// in `Referer` on every outbound link, and in browser history.
|
|
217
|
+
"X-Cbox-Publishable-Key": this.publishableKey,
|
|
218
|
+
Accept: "application/json",
|
|
219
|
+
...extraHeaders
|
|
220
|
+
},
|
|
221
|
+
signal: controller.signal
|
|
222
|
+
});
|
|
223
|
+
} catch (cause) {
|
|
224
|
+
throw new FrontendApiError(
|
|
225
|
+
`Could not reach Cbox ID at ${this.issuer}. If this is a browser, check that this page's origin is on the key's allow-list \u2014 a refused request fails as a network error by design.`,
|
|
226
|
+
"unavailable"
|
|
227
|
+
);
|
|
228
|
+
} finally {
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
}
|
|
231
|
+
if (response.status === 429) {
|
|
232
|
+
const retryAfter = Number(response.headers.get("Retry-After") ?? "");
|
|
233
|
+
throw new FrontendApiError(
|
|
234
|
+
"Cbox ID is rate limiting this key.",
|
|
235
|
+
"rate_limited",
|
|
236
|
+
429,
|
|
237
|
+
Number.isFinite(retryAfter) ? retryAfter : void 0
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (response.status === 401 || response.status === 403) {
|
|
241
|
+
throw new FrontendApiError(
|
|
242
|
+
`Cbox ID refused the request (${response.status}). Check that this page's origin is on the key's allow-list, and that the key is not revoked.`,
|
|
243
|
+
"origin_not_allowed",
|
|
244
|
+
response.status
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new FrontendApiError(
|
|
249
|
+
`Cbox ID returned ${response.status}.`,
|
|
250
|
+
"unavailable",
|
|
251
|
+
response.status
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
let body;
|
|
255
|
+
try {
|
|
256
|
+
body = await response.json();
|
|
257
|
+
} catch {
|
|
258
|
+
throw new FrontendApiError("Cbox ID returned a body that is not JSON.", "malformed", response.status);
|
|
259
|
+
}
|
|
260
|
+
if (validate && !validate(body)) {
|
|
261
|
+
throw new FrontendApiError(
|
|
262
|
+
"Cbox ID returned a body that is not the expected document.",
|
|
263
|
+
"malformed",
|
|
264
|
+
response.status
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
return body;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
function isFrontendConfig(body) {
|
|
271
|
+
if (typeof body !== "object" || body === null) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
const doc = body;
|
|
275
|
+
return typeof doc.issuer === "string" && typeof doc.endpoints === "object" && doc.endpoints !== null;
|
|
276
|
+
}
|
|
277
|
+
function isFrontendSession(body) {
|
|
278
|
+
return typeof body === "object" && body !== null && "user" in body;
|
|
279
|
+
}
|
|
20
280
|
|
|
21
281
|
// src/discovery.ts
|
|
22
282
|
var Discovery = class {
|
|
@@ -180,7 +440,7 @@ var VaultClient = class {
|
|
|
180
440
|
};
|
|
181
441
|
|
|
182
442
|
// src/webhook.ts
|
|
183
|
-
function
|
|
443
|
+
function timingSafeEqual2(a, b) {
|
|
184
444
|
if (a.length !== b.length) {
|
|
185
445
|
return false;
|
|
186
446
|
}
|
|
@@ -225,7 +485,7 @@ async function verifyWebhook(options) {
|
|
|
225
485
|
const expected = toHex(
|
|
226
486
|
await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${timestamp}.${payload}`))
|
|
227
487
|
);
|
|
228
|
-
return
|
|
488
|
+
return timingSafeEqual2(expected, signature);
|
|
229
489
|
}
|
|
230
490
|
|
|
231
491
|
// src/client.ts
|
|
@@ -279,7 +539,15 @@ var CboxIdClient = class {
|
|
|
279
539
|
params.set("max_age", String(options.maxAge));
|
|
280
540
|
}
|
|
281
541
|
const endpoint = await this.discovery.endpoint("authorization_endpoint");
|
|
282
|
-
return {
|
|
542
|
+
return {
|
|
543
|
+
url: `${endpoint}?${params.toString()}`,
|
|
544
|
+
state,
|
|
545
|
+
codeVerifier,
|
|
546
|
+
nonce,
|
|
547
|
+
// Carried through so authenticate() can hold the instance to it. Only present
|
|
548
|
+
// when requested (exactOptionalPropertyTypes).
|
|
549
|
+
...typeof options.maxAge === "number" ? { maxAge: options.maxAge } : {}
|
|
550
|
+
};
|
|
283
551
|
}
|
|
284
552
|
/**
|
|
285
553
|
* Complete login on your callback route: verify state, exchange the code with the
|
|
@@ -307,7 +575,11 @@ var CboxIdClient = class {
|
|
|
307
575
|
}
|
|
308
576
|
let claims = {};
|
|
309
577
|
if (tokens.id_token) {
|
|
310
|
-
claims = await this.verifyIdToken(tokens.id_token, stored.nonce);
|
|
578
|
+
claims = await this.verifyIdToken(tokens.id_token, stored.nonce, stored.maxAge);
|
|
579
|
+
} else if (typeof stored.maxAge === "number") {
|
|
580
|
+
throw new AuthenticationError(
|
|
581
|
+
"A max_age was requested but no id_token was returned, so the authentication age could not be verified."
|
|
582
|
+
);
|
|
311
583
|
}
|
|
312
584
|
claims = { ...claims, ...await this.userinfo(tokens.access_token) };
|
|
313
585
|
const sub = claims["sub"];
|
|
@@ -373,13 +645,30 @@ var CboxIdClient = class {
|
|
|
373
645
|
const base = `${this.config.issuer.replace(/\/$/, "")}${this.accountPath()}`;
|
|
374
646
|
return returnTo ? `${base}?${new URLSearchParams({ return_to: returnTo }).toString()}` : base;
|
|
375
647
|
}
|
|
376
|
-
/**
|
|
377
|
-
|
|
648
|
+
/**
|
|
649
|
+
* The RP-initiated logout URL, or null when the instance advertises none.
|
|
650
|
+
*
|
|
651
|
+
* `client_id` is always sent, even without a `returnTo`: the OP validates
|
|
652
|
+
* `post_logout_redirect_uri` against the registered allow-list of THAT client
|
|
653
|
+
* (OIDC RP-Initiated Logout 1.0 §2). With no way to name the RP the OP cannot
|
|
654
|
+
* check the list, so it silently drops the return URL and strands the user on a
|
|
655
|
+
* bare "you are signed out" page. `idTokenHint` — the user's `id_token`, if you
|
|
656
|
+
* still hold it — is the spec's other way to identify the RP, and additionally
|
|
657
|
+
* tells the OP which subject is logging out.
|
|
658
|
+
*/
|
|
659
|
+
async logoutUrl(returnTo, idTokenHint) {
|
|
378
660
|
const endpoint = await this.discovery.optionalEndpoint("end_session_endpoint");
|
|
379
661
|
if (!endpoint) {
|
|
380
662
|
return null;
|
|
381
663
|
}
|
|
382
|
-
|
|
664
|
+
const params = new URLSearchParams({ client_id: this.config.clientId });
|
|
665
|
+
if (returnTo) {
|
|
666
|
+
params.set("post_logout_redirect_uri", returnTo);
|
|
667
|
+
}
|
|
668
|
+
if (idTokenHint) {
|
|
669
|
+
params.set("id_token_hint", idTokenHint);
|
|
670
|
+
}
|
|
671
|
+
return `${endpoint}?${params.toString()}`;
|
|
383
672
|
}
|
|
384
673
|
/**
|
|
385
674
|
* A machine (client-credentials) access token for calling Cbox ID APIs as your
|
|
@@ -401,11 +690,11 @@ var CboxIdClient = class {
|
|
|
401
690
|
if (!response.ok) {
|
|
402
691
|
throw new AuthenticationError(`Machine token request failed: ${await response.text()}`);
|
|
403
692
|
}
|
|
404
|
-
const
|
|
405
|
-
if (typeof
|
|
693
|
+
const json2 = await response.json();
|
|
694
|
+
if (typeof json2.access_token !== "string") {
|
|
406
695
|
throw new AuthenticationError("The token response had no access_token.");
|
|
407
696
|
}
|
|
408
|
-
return
|
|
697
|
+
return json2.access_token;
|
|
409
698
|
}
|
|
410
699
|
/** The OIDC userinfo claims for an access token. */
|
|
411
700
|
async userinfo(accessToken) {
|
|
@@ -438,6 +727,28 @@ var CboxIdClient = class {
|
|
|
438
727
|
}
|
|
439
728
|
return await response.json();
|
|
440
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* RFC 7009 token revocation (confidential-client auth). Revokes an access or
|
|
732
|
+
* refresh token; revoking a refresh token also drops the whole token family, so
|
|
733
|
+
* this is what a real "sign out everywhere" does. Requires a secret.
|
|
734
|
+
*
|
|
735
|
+
* Per RFC 7009 the server answers 200 for an unknown or already-revoked token, so
|
|
736
|
+
* a successful call means "the token is not valid any more", not "it existed".
|
|
737
|
+
*
|
|
738
|
+
* @param tokenTypeHint which store to search first — only a hint, never required.
|
|
739
|
+
*/
|
|
740
|
+
async revoke(token, tokenTypeHint) {
|
|
741
|
+
const endpoint = await this.discovery.endpoint("revocation_endpoint");
|
|
742
|
+
const basic = btoa(`${this.config.clientId}:${this.requireSecret()}`);
|
|
743
|
+
const body = new URLSearchParams({ token });
|
|
744
|
+
if (tokenTypeHint) {
|
|
745
|
+
body.set("token_type_hint", tokenTypeHint);
|
|
746
|
+
}
|
|
747
|
+
const response = await this.post(endpoint, body, { authorization: `Basic ${basic}` });
|
|
748
|
+
if (!response.ok) {
|
|
749
|
+
throw new AuthenticationError("Revocation request failed.");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
441
752
|
/** Verify a Cbox ID webhook / inline-action signature. See {@link verifyWebhook}. */
|
|
442
753
|
verifyWebhook(options) {
|
|
443
754
|
return verifyWebhook(options);
|
|
@@ -467,7 +778,7 @@ var CboxIdClient = class {
|
|
|
467
778
|
}
|
|
468
779
|
return await response.json();
|
|
469
780
|
}
|
|
470
|
-
async verifyIdToken(idToken, nonce) {
|
|
781
|
+
async verifyIdToken(idToken, nonce, maxAge) {
|
|
471
782
|
const jwksUri = await this.discovery.endpoint("jwks_uri");
|
|
472
783
|
let resolver = this.jwksByUri.get(jwksUri);
|
|
473
784
|
if (!resolver) {
|
|
@@ -478,7 +789,11 @@ var CboxIdClient = class {
|
|
|
478
789
|
try {
|
|
479
790
|
({ payload } = await jose.jwtVerify(idToken, resolver, {
|
|
480
791
|
issuer: this.config.issuer,
|
|
481
|
-
audience: this.config.clientId
|
|
792
|
+
audience: this.config.clientId,
|
|
793
|
+
// Pin the accepted signature algorithms explicitly. `jose` already refuses
|
|
794
|
+
// `alg: none` and key-type confusion, so this is belt-and-braces — but it
|
|
795
|
+
// states the contract in code rather than relying on a library default.
|
|
796
|
+
algorithms: ["RS256", "ES256"]
|
|
482
797
|
}));
|
|
483
798
|
} catch (error) {
|
|
484
799
|
throw new AuthenticationError(`The id_token could not be verified: ${String(error)}`);
|
|
@@ -486,6 +801,21 @@ var CboxIdClient = class {
|
|
|
486
801
|
if (nonce && payload["nonce"] !== nonce) {
|
|
487
802
|
throw new AuthenticationError("The id_token nonce did not match \u2014 possible replay.");
|
|
488
803
|
}
|
|
804
|
+
if (typeof maxAge === "number") {
|
|
805
|
+
const authTime = payload["auth_time"];
|
|
806
|
+
if (typeof authTime !== "number") {
|
|
807
|
+
throw new AuthenticationError(
|
|
808
|
+
"A max_age was requested but the id_token carried no auth_time, so the authentication age could not be verified."
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
const tolerance = this.config.authTimeToleranceSeconds ?? 60;
|
|
812
|
+
const age = Math.floor(Date.now() / 1e3) - authTime;
|
|
813
|
+
if (age > maxAge + tolerance) {
|
|
814
|
+
throw new AuthenticationError(
|
|
815
|
+
`The authentication is ${age}s old but max_age required ${maxAge}s \u2014 the user did not re-authenticate.`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
489
819
|
return payload;
|
|
490
820
|
}
|
|
491
821
|
scopes() {
|
|
@@ -494,7 +824,7 @@ var CboxIdClient = class {
|
|
|
494
824
|
}
|
|
495
825
|
accountPath() {
|
|
496
826
|
const path = this.config.accountPath;
|
|
497
|
-
return path && path !== "" ? `/${path.replace(/^\//, "")}` : "/
|
|
827
|
+
return path && path !== "" ? `/${path.replace(/^\//, "")}` : "/account";
|
|
498
828
|
}
|
|
499
829
|
requireSecret() {
|
|
500
830
|
if (!this.config.clientSecret) {
|
|
@@ -553,7 +883,10 @@ function defineAuthz(declaration) {
|
|
|
553
883
|
const permissions = declaration.permissions ?? [];
|
|
554
884
|
const roles = declaration.roles ?? [];
|
|
555
885
|
assertDeclaration(permissions, roles);
|
|
556
|
-
|
|
886
|
+
if (declaration.legacyLogin) {
|
|
887
|
+
assertLegacyLogin(declaration.legacyLogin);
|
|
888
|
+
}
|
|
889
|
+
return { ...declaration, permissions, roles };
|
|
557
890
|
}
|
|
558
891
|
async function buildManifest(declaration) {
|
|
559
892
|
const permissions = (declaration.permissions ?? []).map(canonicalPermission);
|
|
@@ -562,7 +895,12 @@ async function buildManifest(declaration) {
|
|
|
562
895
|
permissions.sort((a, b) => byteCompare(a.key, b.key));
|
|
563
896
|
roles.sort((a, b) => byteCompare(a.key, b.key));
|
|
564
897
|
const version = (await sha256Hex(canonicalManifestJson(permissions, roles))).slice(0, 16);
|
|
565
|
-
|
|
898
|
+
const manifest = { version, permissions, roles };
|
|
899
|
+
if (declaration.legacyLogin) {
|
|
900
|
+
assertLegacyLogin(declaration.legacyLogin);
|
|
901
|
+
manifest.legacy_login = declaration.legacyLogin;
|
|
902
|
+
}
|
|
903
|
+
return manifest;
|
|
566
904
|
}
|
|
567
905
|
async function publishManifest(config, declaration) {
|
|
568
906
|
if (!config.issuer || !config.clientId || !config.clientSecret) {
|
|
@@ -588,8 +926,8 @@ async function publishManifest(config, declaration) {
|
|
|
588
926
|
if (!response.ok) {
|
|
589
927
|
throw new AuthenticationError(`Manifest push failed: HTTP ${response.status} ${await response.text()}`);
|
|
590
928
|
}
|
|
591
|
-
const
|
|
592
|
-
return
|
|
929
|
+
const json2 = await response.json();
|
|
930
|
+
return json2 !== null && typeof json2 === "object" ? json2 : {};
|
|
593
931
|
}
|
|
594
932
|
async function mintToken(config, discovery, timeoutMs) {
|
|
595
933
|
const response = await fetchWithTimeout(
|
|
@@ -606,13 +944,13 @@ async function mintToken(config, discovery, timeoutMs) {
|
|
|
606
944
|
},
|
|
607
945
|
timeoutMs
|
|
608
946
|
);
|
|
609
|
-
const
|
|
610
|
-
if (typeof
|
|
947
|
+
const json2 = await response.json().catch(() => ({}));
|
|
948
|
+
if (typeof json2.access_token !== "string" || json2.access_token === "") {
|
|
611
949
|
throw new AuthenticationError(
|
|
612
950
|
"Could not obtain an apps.manifest access token \u2014 check the client credentials and that the client holds the apps.manifest scope."
|
|
613
951
|
);
|
|
614
952
|
}
|
|
615
|
-
return
|
|
953
|
+
return json2.access_token;
|
|
616
954
|
}
|
|
617
955
|
function assertDeclaration(permissions, roles) {
|
|
618
956
|
const permissionKeys = /* @__PURE__ */ new Set();
|
|
@@ -671,11 +1009,11 @@ function emptyToNull(value) {
|
|
|
671
1009
|
function byteCompare(a, b) {
|
|
672
1010
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
673
1011
|
}
|
|
674
|
-
function escapeLikePhp(
|
|
1012
|
+
function escapeLikePhp(json2) {
|
|
675
1013
|
let out = "";
|
|
676
|
-
for (let i = 0; i <
|
|
677
|
-
const char =
|
|
678
|
-
const code =
|
|
1014
|
+
for (let i = 0; i < json2.length; i++) {
|
|
1015
|
+
const char = json2.charAt(i);
|
|
1016
|
+
const code = json2.charCodeAt(i);
|
|
679
1017
|
if (char === "/") {
|
|
680
1018
|
out += "\\/";
|
|
681
1019
|
} else if (code > 127) {
|
|
@@ -699,15 +1037,26 @@ async function fetchWithTimeout(url, init, timeoutMs) {
|
|
|
699
1037
|
clearTimeout(timer);
|
|
700
1038
|
}
|
|
701
1039
|
}
|
|
1040
|
+
function assertLegacyLogin(legacy) {
|
|
1041
|
+
if (!legacy.url.startsWith("https://")) {
|
|
1042
|
+
throw new ConfigurationError("legacyLogin.url must be https \u2014 a password must never cross the network in the clear.");
|
|
1043
|
+
}
|
|
1044
|
+
if (!legacy.secret || legacy.secret.length < 32) {
|
|
1045
|
+
throw new ConfigurationError("legacyLogin.secret must be at least 32 characters; it is what proves a request came from Cbox ID.");
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
702
1048
|
|
|
703
1049
|
exports.AuthenticationError = AuthenticationError;
|
|
704
1050
|
exports.CboxIdClient = CboxIdClient;
|
|
705
1051
|
exports.CboxIdError = CboxIdError;
|
|
1052
|
+
exports.CboxIdFrontend = CboxIdFrontend;
|
|
706
1053
|
exports.ConfigurationError = ConfigurationError;
|
|
1054
|
+
exports.FrontendApiError = FrontendApiError;
|
|
707
1055
|
exports.InvalidStateError = InvalidStateError;
|
|
708
1056
|
exports.VaultClient = VaultClient;
|
|
709
1057
|
exports.buildManifest = buildManifest;
|
|
710
1058
|
exports.challenge = challenge;
|
|
1059
|
+
exports.createLegacyVerifier = createLegacyVerifier;
|
|
711
1060
|
exports.createVerifier = createVerifier;
|
|
712
1061
|
exports.defineAuthz = defineAuthz;
|
|
713
1062
|
exports.publishManifest = publishManifest;
|