@darkauth/client 1.12.0 → 1.13.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/dist/index.d.ts +1 -1
- package/dist/index.js +118 -52
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -31,13 +31,13 @@ let cfg = {
|
|
|
31
31
|
: "http://localhost:5173/callback"),
|
|
32
32
|
zk: true,
|
|
33
33
|
};
|
|
34
|
-
const OBFUSCATION_KEY = "DarkAuth-Storage-Protection-2025";
|
|
35
34
|
const EMPTY_DRK = new Uint8Array(0);
|
|
36
35
|
const ID_TOKEN_KEY = "id_token";
|
|
37
36
|
const ACCESS_TOKEN_KEY = "access_token";
|
|
38
37
|
const REFRESH_TOKEN_KEY = "refresh_token";
|
|
39
38
|
const DRK_STORAGE_KEY = "drk_protected";
|
|
40
39
|
const OAUTH_STATE_KEY = "oauth_state";
|
|
40
|
+
const V2_KEY_JWE_MAX_TTL_SECONDS = 600;
|
|
41
41
|
let memorySession = null;
|
|
42
42
|
let memoryRefreshToken = null;
|
|
43
43
|
export function setConfig(next) {
|
|
@@ -66,9 +66,6 @@ function clearStoredAccessToken() {
|
|
|
66
66
|
function tokenStorageMode() {
|
|
67
67
|
return cfg.tokenStorage || (cfg.firstParty === false ? "localStorage" : "memory");
|
|
68
68
|
}
|
|
69
|
-
function drkStorageMode() {
|
|
70
|
-
return cfg.drkStorage || (cfg.firstParty === false ? "localStorage" : "memory");
|
|
71
|
-
}
|
|
72
69
|
function refreshMode() {
|
|
73
70
|
return cfg.refreshMode || (cfg.firstParty === false ? "token" : "cookie");
|
|
74
71
|
}
|
|
@@ -150,42 +147,44 @@ function parseFragmentParams(hash) {
|
|
|
150
147
|
}
|
|
151
148
|
return res;
|
|
152
149
|
}
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
150
|
+
async function sha256Base64Url(value) {
|
|
151
|
+
return bytesToBase64Url(await sha256(new TextEncoder().encode(value)));
|
|
152
|
+
}
|
|
153
|
+
function isStringArray(value) {
|
|
154
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
155
|
+
}
|
|
156
|
+
function audienceMatches(value, expected) {
|
|
157
|
+
if (typeof value === "string")
|
|
158
|
+
return value === expected;
|
|
159
|
+
if (isStringArray(value))
|
|
160
|
+
return value.includes(expected);
|
|
161
|
+
return false;
|
|
162
162
|
}
|
|
163
|
-
function
|
|
164
|
-
|
|
163
|
+
function requireString(value, name) {
|
|
164
|
+
if (typeof value !== "string" || value.length === 0)
|
|
165
|
+
throw new Error(`Invalid ${name}`);
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
function requireNumber(value, name) {
|
|
169
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
170
|
+
throw new Error(`Invalid ${name}`);
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
function parseJsonPayload(bytes) {
|
|
174
|
+
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
175
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
176
|
+
throw new Error("Invalid key payload");
|
|
177
|
+
return parsed;
|
|
165
178
|
}
|
|
166
179
|
function clearStoredDrk() {
|
|
167
180
|
localStorage.removeItem(DRK_STORAGE_KEY);
|
|
168
181
|
}
|
|
169
182
|
function getStoredDrk() {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
return null;
|
|
173
|
-
}
|
|
174
|
-
const obfuscatedDrkBase64 = localStorage.getItem(DRK_STORAGE_KEY);
|
|
175
|
-
if (!obfuscatedDrkBase64)
|
|
176
|
-
return null;
|
|
177
|
-
try {
|
|
178
|
-
const obfuscatedDrk = base64UrlToBytes(obfuscatedDrkBase64);
|
|
179
|
-
return deobfuscateKey(obfuscatedDrk);
|
|
180
|
-
}
|
|
181
|
-
catch {
|
|
182
|
-
clearStoredDrk();
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
183
|
+
clearStoredDrk();
|
|
184
|
+
return null;
|
|
185
185
|
}
|
|
186
186
|
function storeSession(session) {
|
|
187
187
|
const tokenMode = tokenStorageMode();
|
|
188
|
-
const drkMode = drkStorageMode();
|
|
189
188
|
const currentRefreshMode = refreshMode();
|
|
190
189
|
const storedSession = {
|
|
191
190
|
idToken: session.idToken,
|
|
@@ -205,13 +204,7 @@ function storeSession(session) {
|
|
|
205
204
|
clearStoredIdToken();
|
|
206
205
|
clearStoredAccessToken();
|
|
207
206
|
}
|
|
208
|
-
|
|
209
|
-
const obfuscatedDrk = obfuscateKey(session.drk);
|
|
210
|
-
localStorage.setItem(DRK_STORAGE_KEY, bytesToBase64Url(obfuscatedDrk));
|
|
211
|
-
}
|
|
212
|
-
else {
|
|
213
|
-
clearStoredDrk();
|
|
214
|
-
}
|
|
207
|
+
clearStoredDrk();
|
|
215
208
|
if (currentRefreshMode === "token") {
|
|
216
209
|
memoryRefreshToken = session.refreshToken || memoryRefreshToken;
|
|
217
210
|
if (tokenMode === "localStorage" && session.refreshToken) {
|
|
@@ -229,12 +222,13 @@ function clearCallbackStorage() {
|
|
|
229
222
|
sessionStorage.removeItem(OAUTH_STATE_KEY);
|
|
230
223
|
sessionStorage.removeItem("pkce_verifier");
|
|
231
224
|
}
|
|
232
|
-
function
|
|
233
|
-
if (!location.hash.includes("drk_jwe="))
|
|
225
|
+
function stripKeyJweFragment() {
|
|
226
|
+
if (!location.hash.includes("drk_jwe=") && !location.hash.includes("darkauth_key_jwe="))
|
|
234
227
|
return;
|
|
235
228
|
const hash = location.hash.startsWith("#") ? location.hash.slice(1) : location.hash;
|
|
236
229
|
const params = new URLSearchParams(hash);
|
|
237
230
|
params.delete("drk_jwe");
|
|
231
|
+
params.delete("darkauth_key_jwe");
|
|
238
232
|
const nextHash = params.toString();
|
|
239
233
|
const nextUrl = `${location.origin}${location.pathname}${location.search || ""}${nextHash ? `#${nextHash}` : ""}`;
|
|
240
234
|
try {
|
|
@@ -307,8 +301,9 @@ export async function handleCallback() {
|
|
|
307
301
|
return null;
|
|
308
302
|
const fragmentParams = parseFragmentParams(location.hash || "");
|
|
309
303
|
const drkJwe = fragmentParams.drk_jwe;
|
|
310
|
-
|
|
311
|
-
|
|
304
|
+
const darkauthKeyJwe = fragmentParams.darkauth_key_jwe;
|
|
305
|
+
if (drkJwe || darkauthKeyJwe)
|
|
306
|
+
stripKeyJweFragment();
|
|
312
307
|
const expectedState = sessionStorage.getItem(OAUTH_STATE_KEY);
|
|
313
308
|
const returnedState = params.get("state");
|
|
314
309
|
if (!expectedState)
|
|
@@ -342,6 +337,15 @@ export async function handleCallback() {
|
|
|
342
337
|
const zkDrkHash = typeof tokenResponse.zk_drk_hash === "string"
|
|
343
338
|
? tokenResponse.zk_drk_hash
|
|
344
339
|
: null;
|
|
340
|
+
const zkKeyHash = typeof tokenResponse.zk_key_hash === "string"
|
|
341
|
+
? tokenResponse.zk_key_hash
|
|
342
|
+
: null;
|
|
343
|
+
const zkKeyKind = typeof tokenResponse.zk_key_kind === "string"
|
|
344
|
+
? tokenResponse.zk_key_kind
|
|
345
|
+
: null;
|
|
346
|
+
const zkKeyVersion = typeof tokenResponse.zk_key_version === "string"
|
|
347
|
+
? tokenResponse.zk_key_version
|
|
348
|
+
: null;
|
|
345
349
|
const idToken = tokenResponse.id_token;
|
|
346
350
|
const accessToken = typeof tokenResponse.access_token === "string"
|
|
347
351
|
? tokenResponse.access_token
|
|
@@ -350,25 +354,87 @@ export async function handleCallback() {
|
|
|
350
354
|
const refreshToken = tokenRefreshMode === "token"
|
|
351
355
|
? tokenResponse.refresh_token
|
|
352
356
|
: undefined;
|
|
353
|
-
const
|
|
357
|
+
const hasV2Artifacts = !!darkauthKeyJwe || !!zkKeyHash || !!zkKeyKind || !!zkKeyVersion;
|
|
358
|
+
const hasLegacyArtifacts = !!drkJwe || !!zkDrkHash;
|
|
359
|
+
const hasZkArtifacts = hasV2Artifacts || hasLegacyArtifacts;
|
|
354
360
|
if (!hasZkArtifacts) {
|
|
355
361
|
clearCallbackUrl();
|
|
356
362
|
return storeSession({ idToken, accessToken, drk: EMPTY_DRK, refreshToken });
|
|
357
363
|
}
|
|
358
|
-
if (!drkJwe || typeof drkJwe !== "string")
|
|
359
|
-
throw new Error("Missing DRK JWE from URL fragment");
|
|
360
|
-
if (zkDrkHash) {
|
|
361
|
-
const hash = bytesToBase64Url(await sha256(new TextEncoder().encode(drkJwe)));
|
|
362
|
-
if (zkDrkHash !== hash)
|
|
363
|
-
throw new Error("DRK hash mismatch");
|
|
364
|
-
}
|
|
365
364
|
const privateJwkString = sessionStorage.getItem("zk_eph_priv_jwk");
|
|
366
365
|
if (!privateJwkString)
|
|
367
366
|
throw new Error("Missing ZK private key for callback");
|
|
368
367
|
sessionStorage.removeItem("zk_eph_priv_jwk");
|
|
369
368
|
const privateKey = await crypto.subtle.importKey("jwk", JSON.parse(privateJwkString), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits", "deriveKey"]);
|
|
370
|
-
|
|
371
|
-
|
|
369
|
+
let drk;
|
|
370
|
+
if (hasV2Artifacts) {
|
|
371
|
+
if (hasLegacyArtifacts)
|
|
372
|
+
throw new Error("Mixed key delivery metadata");
|
|
373
|
+
if (!darkauthKeyJwe || typeof darkauthKeyJwe !== "string")
|
|
374
|
+
throw new Error("Missing client key JWE from URL fragment");
|
|
375
|
+
if (!zkKeyHash)
|
|
376
|
+
throw new Error("Missing client key hash");
|
|
377
|
+
if (zkKeyKind !== "client_app_key")
|
|
378
|
+
throw new Error("Invalid client key kind");
|
|
379
|
+
if (zkKeyVersion !== "v2")
|
|
380
|
+
throw new Error("Invalid client key version");
|
|
381
|
+
const hash = await sha256Base64Url(darkauthKeyJwe);
|
|
382
|
+
if (zkKeyHash !== hash)
|
|
383
|
+
throw new Error("Client key hash mismatch");
|
|
384
|
+
const { plaintext, protectedHeader } = await compactDecrypt(darkauthKeyJwe, privateKey);
|
|
385
|
+
if (protectedHeader.alg !== "ECDH-ES" || protectedHeader.enc !== "A256GCM")
|
|
386
|
+
throw new Error("Invalid client key JWE header");
|
|
387
|
+
const payload = parseJsonPayload(new Uint8Array(plaintext));
|
|
388
|
+
if (payload.typ !== "DarkAuth-Client-Key")
|
|
389
|
+
throw new Error("Invalid client key type");
|
|
390
|
+
if (payload.version !== "v2" && payload.version !== "v2-client-key")
|
|
391
|
+
throw new Error("Invalid client key payload version");
|
|
392
|
+
if (payload.key_kind !== "client_app_key")
|
|
393
|
+
throw new Error("Invalid client key payload kind");
|
|
394
|
+
if (payload.client_id !== cfg.clientId)
|
|
395
|
+
throw new Error("Invalid client key client");
|
|
396
|
+
if (!audienceMatches(payload.aud, cfg.clientId))
|
|
397
|
+
throw new Error("Invalid client key audience");
|
|
398
|
+
const idTokenSubject = parseJwt(idToken)?.sub;
|
|
399
|
+
if (!idTokenSubject || payload.sub !== idTokenSubject)
|
|
400
|
+
throw new Error("Invalid client key subject");
|
|
401
|
+
if (payload.state_hash !== (await sha256Base64Url(expectedState)))
|
|
402
|
+
throw new Error("Invalid client key state");
|
|
403
|
+
if (payload.redirect_uri_hash !== (await sha256Base64Url(cfg.redirectUri)))
|
|
404
|
+
throw new Error("Invalid client key redirect URI");
|
|
405
|
+
requireString(payload.key_id, "client key id");
|
|
406
|
+
const exp = requireNumber(payload.exp, "client key expiry");
|
|
407
|
+
const now = Date.now() / 1000;
|
|
408
|
+
if (exp <= now)
|
|
409
|
+
throw new Error("Client key JWE expired");
|
|
410
|
+
if (typeof payload.iat === "number") {
|
|
411
|
+
if (!Number.isFinite(payload.iat))
|
|
412
|
+
throw new Error("Invalid client key issued-at");
|
|
413
|
+
if (payload.iat > now + 60)
|
|
414
|
+
throw new Error("Invalid client key issued-at");
|
|
415
|
+
if (exp - payload.iat > V2_KEY_JWE_MAX_TTL_SECONDS)
|
|
416
|
+
throw new Error("Client key JWE lifetime too long");
|
|
417
|
+
}
|
|
418
|
+
else if (exp > now + V2_KEY_JWE_MAX_TTL_SECONDS) {
|
|
419
|
+
throw new Error("Client key JWE lifetime too long");
|
|
420
|
+
}
|
|
421
|
+
drk = base64UrlToBytes(requireString(payload.cak, "client app key"));
|
|
422
|
+
if (drk.length === 0)
|
|
423
|
+
throw new Error("Invalid client app key");
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
if (!drkJwe || typeof drkJwe !== "string")
|
|
427
|
+
throw new Error("Missing DRK JWE from URL fragment");
|
|
428
|
+
if (zkDrkHash) {
|
|
429
|
+
const hash = await sha256Base64Url(drkJwe);
|
|
430
|
+
if (zkDrkHash !== hash)
|
|
431
|
+
throw new Error("DRK hash mismatch");
|
|
432
|
+
}
|
|
433
|
+
const { plaintext, protectedHeader } = await compactDecrypt(drkJwe, privateKey);
|
|
434
|
+
if (protectedHeader.alg !== "ECDH-ES" || protectedHeader.enc !== "A256GCM")
|
|
435
|
+
throw new Error("Invalid DRK JWE header");
|
|
436
|
+
drk = new Uint8Array(plaintext);
|
|
437
|
+
}
|
|
372
438
|
clearCallbackUrl();
|
|
373
439
|
return storeSession({ idToken, accessToken, drk, refreshToken });
|
|
374
440
|
}
|