@darkauth/client 1.7.1 → 1.8.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 CHANGED
@@ -10,9 +10,9 @@ The client supports both:
10
10
 
11
11
  - **Zero-Knowledge Authentication**: Secure OAuth2/OIDC flow with PKCE and ephemeral key exchange
12
12
  - **Client-Side Encryption**: Built-in cryptographic functions for data encryption/decryption
13
- - **Token Management**: Automatic token storage, validation, and refresh
13
+ - **Token Management**: First-party cookie refresh by default, with optional legacy token storage
14
14
  - **Data Encryption Keys (DEK)**: Support for deriving and managing data encryption keys
15
- - **Session Persistence**: Secure session storage with key obfuscation
15
+ - **DRK Custody**: Memory-only DRK handling by default for hosted web zero-knowledge apps
16
16
  - **TypeScript Support**: Full TypeScript definitions included
17
17
 
18
18
  ## Installation
@@ -42,10 +42,10 @@ await initiateLogin();
42
42
  // Handle OAuth callback (on your callback page)
43
43
  const session = await handleCallback();
44
44
  if (session) {
45
- console.log('Logged in!', session.idToken);
45
+ console.log('Logged in!', session.accessToken);
46
46
  }
47
47
 
48
- // Get existing session
48
+ // Get existing in-memory session
49
49
  const existingSession = getStoredSession();
50
50
  if (existingSession && isTokenValid(existingSession.idToken)) {
51
51
  // User is authenticated
@@ -65,7 +65,13 @@ setConfig({
65
65
  issuer: 'https://auth.example.com', // DarkAuth server URL
66
66
  clientId: 'your-client-id', // Your application's client ID
67
67
  redirectUri: 'https://app.example.com/callback', // OAuth callback URL
68
- zk: true // Optional. Default true. Set false for non-ZK flows.
68
+ scope: 'openid profile email', // Optional OAuth scopes
69
+ zk: true, // Optional. Default true. Set false for non-ZK flows.
70
+ firstParty: true, // Optional. Default true. Uses cookie refresh and memory storage.
71
+ tokenStorage: 'memory', // Optional. Default 'memory'. Use 'localStorage' only for legacy flows.
72
+ drkStorage: 'memory', // Optional. Default 'memory'. Use 'localStorage' only for explicit convenience mode.
73
+ refreshMode: 'cookie', // Optional. Default 'cookie'. Use 'token' only for legacy refresh-token clients.
74
+ credentials: 'include' // Optional. Default 'include' for cookie refresh.
69
75
  });
70
76
  ```
71
77
 
@@ -84,24 +90,30 @@ Starts the OAuth2/OIDC login flow with PKCE. Redirects the user to the DarkAuth
84
90
 
85
91
  Processes the OAuth callback after successful authentication. Returns an `AuthSession` object containing:
86
92
  - `idToken`: JWT ID token
93
+ - `accessToken?`: OAuth access token for API authorization
87
94
  - `drk`: Derived Root Key for encryption operations. In non-ZK flows this is an empty `Uint8Array`.
88
95
  - `refreshToken?`: Optional refresh token
89
96
 
90
97
  Behavior:
98
+ - OAuth `state` is validated before exchanging the authorization code.
91
99
  - If ZK artifacts are present in the callback/token response, ZK validation and DRK decryption are enforced.
92
100
  - If no ZK artifacts are present, callback still succeeds as a standard OIDC flow.
101
+ - In default first-party mode, tokens and DRK are kept in memory and refresh uses `HttpOnly` cookies set by DarkAuth.
102
+ - Legacy `localStorage` token or DRK persistence is available only when explicitly configured.
93
103
 
94
104
  #### `logout(): void`
95
105
 
96
- Clears all authentication data from storage.
106
+ Clears the in-memory session, callback state, PKCE verifier, ephemeral ZK key, and any explicitly configured legacy storage.
97
107
 
98
108
  #### `getStoredSession(): AuthSession | null`
99
109
 
100
- Retrieves the current session from storage if valid. For non-ZK sessions, returns `drk` as an empty `Uint8Array`.
110
+ Retrieves the current in-memory session if valid. For non-ZK sessions, returns `drk` as an empty `Uint8Array`.
111
+
112
+ If `tokenStorage: 'localStorage'` or `drkStorage: 'localStorage'` is configured for a legacy app, this function can also restore those explicitly persisted values.
101
113
 
102
114
  #### `refreshSession(): Promise<AuthSession | null>`
103
115
 
104
- Refreshes the current session using the stored refresh token. For non-ZK sessions, returns `drk` as an empty `Uint8Array`.
116
+ Refreshes the current session. In default first-party mode, the browser sends the DarkAuth refresh cookie and no JavaScript-readable refresh token is required. For non-ZK sessions, returns `drk` as an empty `Uint8Array`.
105
117
 
106
118
  ### User Information
107
119
 
@@ -181,6 +193,7 @@ setHooks({
181
193
  ```typescript
182
194
  interface AuthSession {
183
195
  idToken: string;
196
+ accessToken?: string;
184
197
  drk: Uint8Array;
185
198
  refreshToken?: string;
186
199
  }
@@ -205,6 +218,11 @@ type Config = {
205
218
  clientId: string;
206
219
  redirectUri: string;
207
220
  zk?: boolean;
221
+ firstParty?: boolean;
222
+ tokenStorage?: 'memory' | 'localStorage';
223
+ drkStorage?: 'memory' | 'localStorage';
224
+ refreshMode?: 'cookie' | 'token';
225
+ credentials?: RequestCredentials;
208
226
  }
209
227
  ```
210
228
 
@@ -220,16 +238,26 @@ type ClientHooks = {
220
238
 
221
239
  - **PKCE (Proof Key for Code Exchange)**: Protects against authorization code interception
222
240
  - **Zero-Knowledge Mode**: Ephemeral key exchange for enhanced privacy
223
- - **Key Obfuscation**: DRK is obfuscated in storage for additional protection
224
- - **Secure Storage**: Uses sessionStorage for tokens and localStorage for persistent data
241
+ - **State Validation**: Verifies OAuth state before token exchange
242
+ - **First-Party Cookie Refresh**: Supports `HttpOnly` refresh cookies instead of JavaScript-readable refresh tokens
243
+ - **Memory-Only DRK Default**: Keeps the DRK out of persistent browser storage unless explicitly configured otherwise
225
244
  - **AEAD Encryption**: AES-GCM with additional authenticated data for all encryption operations
226
245
 
246
+ ## Custody Model
247
+
248
+ Auth and session tokens are not the same as the DRK.
249
+
250
+ In the default first-party hosted-web profile, DarkAuth protects refresh credentials with `HttpOnly` cookies and the SDK keeps the active ID/access token view in memory. The DRK is returned to the app because the app's browser code needs it to decrypt user data. That DRK is also memory-only by default. A page reload loses it and the app should start a fresh authorization request with a new ephemeral `zk_pub`.
251
+
252
+ This model supports the hosted-web zero-knowledge claim for honest operation: the DarkAuth backend and app backend do not receive the user's password, OPAQUE export key, plaintext DRK, or plaintext app data. It still requires trusting the browser, the user's device, and the JavaScript served by the trusted origins.
253
+
227
254
  ## Browser Compatibility
228
255
 
229
256
  This library requires a modern browser with support for:
230
257
  - Web Crypto API
231
258
  - ES2015+ features
232
- - SessionStorage and LocalStorage
259
+ - SessionStorage
260
+ - LocalStorage only when explicitly using legacy persistence options
233
261
 
234
262
  ## Development
235
263
 
package/dist/index.d.ts CHANGED
@@ -5,10 +5,20 @@ type Config = {
5
5
  issuer: string;
6
6
  clientId: string;
7
7
  redirectUri: string;
8
+ scope?: string;
8
9
  zk?: boolean;
10
+ authorizationEndpoint?: string;
11
+ tokenEndpoint?: string;
12
+ discovery?: boolean;
13
+ firstParty?: boolean;
14
+ tokenStorage?: "memory" | "localStorage";
15
+ drkStorage?: "memory" | "localStorage";
16
+ refreshMode?: "cookie" | "token";
17
+ credentials?: RequestCredentials;
9
18
  };
10
19
  export interface AuthSession {
11
20
  idToken: string;
21
+ accessToken?: string;
12
22
  drk: Uint8Array;
13
23
  refreshToken?: string;
14
24
  }
package/dist/index.js CHANGED
@@ -13,6 +13,8 @@ function viteEnvGet(key) {
13
13
  }
14
14
  let callbackInFlight = null;
15
15
  let callbackInFlightCode = null;
16
+ let endpointsInFlight = null;
17
+ let endpointsCacheKey = null;
16
18
  let cfg = {
17
19
  issuer: (typeof window !== "undefined" && window.__APP_CONFIG__?.issuer) ||
18
20
  viteEnvGet("VITE_DARKAUTH_ISSUER") ||
@@ -32,8 +34,16 @@ let cfg = {
32
34
  const OBFUSCATION_KEY = "DarkAuth-Storage-Protection-2025";
33
35
  const EMPTY_DRK = new Uint8Array(0);
34
36
  const ID_TOKEN_KEY = "id_token";
37
+ const ACCESS_TOKEN_KEY = "access_token";
38
+ const REFRESH_TOKEN_KEY = "refresh_token";
39
+ const DRK_STORAGE_KEY = "drk_protected";
40
+ const OAUTH_STATE_KEY = "oauth_state";
41
+ let memorySession = null;
42
+ let memoryRefreshToken = null;
35
43
  export function setConfig(next) {
36
44
  cfg = { ...cfg, ...next };
45
+ endpointsInFlight = null;
46
+ endpointsCacheKey = null;
37
47
  }
38
48
  function setStoredIdToken(token) {
39
49
  localStorage.setItem(ID_TOKEN_KEY, token);
@@ -44,6 +54,73 @@ function getStoredIdToken() {
44
54
  function clearStoredIdToken() {
45
55
  localStorage.removeItem(ID_TOKEN_KEY);
46
56
  }
57
+ function setStoredAccessToken(token) {
58
+ localStorage.setItem(ACCESS_TOKEN_KEY, token);
59
+ }
60
+ function getStoredAccessToken() {
61
+ return localStorage.getItem(ACCESS_TOKEN_KEY);
62
+ }
63
+ function clearStoredAccessToken() {
64
+ localStorage.removeItem(ACCESS_TOKEN_KEY);
65
+ }
66
+ function tokenStorageMode() {
67
+ return cfg.tokenStorage || (cfg.firstParty === false ? "localStorage" : "memory");
68
+ }
69
+ function drkStorageMode() {
70
+ return cfg.drkStorage || (cfg.firstParty === false ? "localStorage" : "memory");
71
+ }
72
+ function refreshMode() {
73
+ return cfg.refreshMode || (cfg.firstParty === false ? "token" : "cookie");
74
+ }
75
+ function fetchCredentials() {
76
+ return cfg.credentials || "include";
77
+ }
78
+ function rootEndpoint(path) {
79
+ return new URL(path, cfg.issuer).toString();
80
+ }
81
+ async function resolveEndpoints() {
82
+ const cacheKey = [
83
+ cfg.issuer,
84
+ cfg.scope || "",
85
+ cfg.authorizationEndpoint || "",
86
+ cfg.tokenEndpoint || "",
87
+ cfg.discovery === false ? "0" : "1",
88
+ ].join("|");
89
+ if (endpointsInFlight && endpointsCacheKey === cacheKey)
90
+ return endpointsInFlight;
91
+ endpointsCacheKey = cacheKey;
92
+ endpointsInFlight = (async () => {
93
+ const fallback = {
94
+ authorizationEndpoint: cfg.authorizationEndpoint || rootEndpoint("/authorize"),
95
+ tokenEndpoint: cfg.tokenEndpoint || rootEndpoint("/token"),
96
+ };
97
+ if (cfg.authorizationEndpoint && cfg.tokenEndpoint)
98
+ return fallback;
99
+ if (cfg.discovery === false || typeof fetch !== "function")
100
+ return fallback;
101
+ try {
102
+ const discoveryUrl = new URL("/.well-known/openid-configuration", cfg.issuer);
103
+ const response = await fetch(discoveryUrl.toString());
104
+ if (!response.ok)
105
+ return fallback;
106
+ const metadata = (await response.json());
107
+ return {
108
+ authorizationEndpoint: cfg.authorizationEndpoint ||
109
+ (typeof metadata.authorization_endpoint === "string"
110
+ ? metadata.authorization_endpoint
111
+ : fallback.authorizationEndpoint),
112
+ tokenEndpoint: cfg.tokenEndpoint ||
113
+ (typeof metadata.token_endpoint === "string"
114
+ ? metadata.token_endpoint
115
+ : fallback.tokenEndpoint),
116
+ };
117
+ }
118
+ catch {
119
+ return fallback;
120
+ }
121
+ })();
122
+ return endpointsInFlight;
123
+ }
47
124
  function bytesToBase64Url(bytes) {
48
125
  let s = "";
49
126
  for (const b of bytes)
@@ -86,6 +163,91 @@ function obfuscateKey(drk) {
86
163
  function deobfuscateKey(obfuscated) {
87
164
  return obfuscateKey(obfuscated);
88
165
  }
166
+ function clearStoredDrk() {
167
+ localStorage.removeItem(DRK_STORAGE_KEY);
168
+ }
169
+ function getStoredDrk() {
170
+ if (drkStorageMode() !== "localStorage") {
171
+ clearStoredDrk();
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
+ }
185
+ }
186
+ function storeSession(session) {
187
+ const tokenMode = tokenStorageMode();
188
+ const drkMode = drkStorageMode();
189
+ const currentRefreshMode = refreshMode();
190
+ const storedSession = {
191
+ idToken: session.idToken,
192
+ accessToken: session.accessToken,
193
+ drk: session.drk,
194
+ refreshToken: currentRefreshMode === "token" ? session.refreshToken : undefined,
195
+ };
196
+ memorySession = storedSession;
197
+ if (tokenMode === "localStorage") {
198
+ setStoredIdToken(session.idToken);
199
+ if (session.accessToken)
200
+ setStoredAccessToken(session.accessToken);
201
+ else
202
+ clearStoredAccessToken();
203
+ }
204
+ else {
205
+ clearStoredIdToken();
206
+ clearStoredAccessToken();
207
+ }
208
+ if (drkMode === "localStorage" && session.drk.length > 0) {
209
+ const obfuscatedDrk = obfuscateKey(session.drk);
210
+ localStorage.setItem(DRK_STORAGE_KEY, bytesToBase64Url(obfuscatedDrk));
211
+ }
212
+ else {
213
+ clearStoredDrk();
214
+ }
215
+ if (currentRefreshMode === "token") {
216
+ memoryRefreshToken = session.refreshToken || memoryRefreshToken;
217
+ if (tokenMode === "localStorage" && session.refreshToken) {
218
+ localStorage.setItem(REFRESH_TOKEN_KEY, session.refreshToken);
219
+ }
220
+ }
221
+ else {
222
+ memoryRefreshToken = null;
223
+ localStorage.removeItem(REFRESH_TOKEN_KEY);
224
+ }
225
+ return storedSession;
226
+ }
227
+ function clearCallbackStorage() {
228
+ sessionStorage.removeItem("zk_eph_priv_jwk");
229
+ sessionStorage.removeItem(OAUTH_STATE_KEY);
230
+ sessionStorage.removeItem("pkce_verifier");
231
+ }
232
+ function stripDrkJweFragment() {
233
+ if (!location.hash.includes("drk_jwe="))
234
+ return;
235
+ const hash = location.hash.startsWith("#") ? location.hash.slice(1) : location.hash;
236
+ const params = new URLSearchParams(hash);
237
+ params.delete("drk_jwe");
238
+ const nextHash = params.toString();
239
+ const nextUrl = `${location.origin}${location.pathname}${location.search || ""}${nextHash ? `#${nextHash}` : ""}`;
240
+ try {
241
+ history.replaceState(null, "", nextUrl);
242
+ }
243
+ catch { }
244
+ }
245
+ function clearCallbackUrl() {
246
+ try {
247
+ history.replaceState(null, "", location.origin + location.pathname);
248
+ }
249
+ catch { }
250
+ }
89
251
  export function parseJwt(token) {
90
252
  try {
91
253
  const parts = token.split(".");
@@ -120,13 +282,15 @@ export async function initiateLogin() {
120
282
  }
121
283
  const state = crypto.randomUUID();
122
284
  const verifier = bytesToBase64Url(crypto.getRandomValues(new Uint8Array(32)));
285
+ sessionStorage.setItem(OAUTH_STATE_KEY, state);
123
286
  sessionStorage.setItem("pkce_verifier", verifier);
124
287
  const challenge = bytesToBase64Url(await sha256(new TextEncoder().encode(verifier)));
125
- const authUrl = new URL("/authorize", cfg.issuer);
288
+ const endpoints = await resolveEndpoints();
289
+ const authUrl = new URL(endpoints.authorizationEndpoint);
126
290
  authUrl.searchParams.set("client_id", cfg.clientId);
127
291
  authUrl.searchParams.set("redirect_uri", cfg.redirectUri);
128
292
  authUrl.searchParams.set("response_type", "code");
129
- authUrl.searchParams.set("scope", "openid profile");
293
+ authUrl.searchParams.set("scope", cfg.scope || "openid profile");
130
294
  authUrl.searchParams.set("state", state);
131
295
  authUrl.searchParams.set("code_challenge", challenge);
132
296
  authUrl.searchParams.set("code_challenge_method", "S256");
@@ -141,69 +305,76 @@ export async function handleCallback() {
141
305
  const code = params.get("code");
142
306
  if (!code)
143
307
  return null;
308
+ const fragmentParams = parseFragmentParams(location.hash || "");
309
+ const drkJwe = fragmentParams.drk_jwe;
310
+ if (drkJwe)
311
+ stripDrkJweFragment();
312
+ const expectedState = sessionStorage.getItem(OAUTH_STATE_KEY);
313
+ const returnedState = params.get("state");
314
+ if (!expectedState)
315
+ throw new Error("Missing OAuth state");
316
+ if (!returnedState || returnedState !== expectedState)
317
+ throw new Error("Invalid OAuth state");
144
318
  if (callbackInFlight && callbackInFlightCode === code) {
145
319
  return callbackInFlight;
146
320
  }
147
321
  const exchangePromise = (async () => {
148
- const tokenUrl = new URL("/token", cfg.issuer);
149
- const verifier = sessionStorage.getItem("pkce_verifier") || "";
150
- const response = await fetch(tokenUrl.toString(), {
151
- method: "POST",
152
- headers: { "content-type": "application/x-www-form-urlencoded" },
153
- body: new URLSearchParams({
154
- grant_type: "authorization_code",
155
- code,
156
- client_id: cfg.clientId,
157
- redirect_uri: cfg.redirectUri,
158
- code_verifier: verifier,
159
- }),
160
- });
161
- if (!response.ok) {
162
- throw new Error("Token exchange failed");
163
- }
164
- const tokenResponse = await response.json();
165
- const fragmentParams = parseFragmentParams(location.hash || "");
166
- const drkJwe = fragmentParams.drk_jwe;
167
- const zkDrkHash = typeof tokenResponse.zk_drk_hash === "string" ? tokenResponse.zk_drk_hash : null;
168
- const idToken = tokenResponse.id_token;
169
- const refreshToken = tokenResponse.refresh_token;
170
- const hasZkArtifacts = !!drkJwe || !!zkDrkHash;
171
- if (!hasZkArtifacts) {
172
- sessionStorage.removeItem("zk_eph_priv_jwk");
173
- try {
174
- history.replaceState(null, "", location.origin + location.pathname);
322
+ try {
323
+ const endpoints = await resolveEndpoints();
324
+ const tokenUrl = new URL(endpoints.tokenEndpoint);
325
+ const verifier = sessionStorage.getItem("pkce_verifier") || "";
326
+ const response = await fetch(tokenUrl.toString(), {
327
+ method: "POST",
328
+ headers: { "content-type": "application/x-www-form-urlencoded" },
329
+ body: new URLSearchParams({
330
+ grant_type: "authorization_code",
331
+ code,
332
+ client_id: cfg.clientId,
333
+ redirect_uri: cfg.redirectUri,
334
+ code_verifier: verifier,
335
+ }),
336
+ credentials: fetchCredentials(),
337
+ });
338
+ if (!response.ok) {
339
+ throw new Error("Token exchange failed");
175
340
  }
176
- catch { }
177
- setStoredIdToken(idToken);
178
- localStorage.removeItem("drk_protected");
179
- if (refreshToken)
180
- localStorage.setItem("refresh_token", refreshToken);
181
- return { idToken, drk: EMPTY_DRK, refreshToken };
182
- }
183
- if (!drkJwe || typeof drkJwe !== "string")
184
- throw new Error("Missing DRK JWE from URL fragment");
185
- if (zkDrkHash) {
186
- const hash = bytesToBase64Url(await sha256(new TextEncoder().encode(drkJwe)));
187
- if (zkDrkHash !== hash)
188
- throw new Error("DRK hash mismatch");
341
+ const tokenResponse = await response.json();
342
+ const zkDrkHash = typeof tokenResponse.zk_drk_hash === "string"
343
+ ? tokenResponse.zk_drk_hash
344
+ : null;
345
+ const idToken = tokenResponse.id_token;
346
+ const accessToken = typeof tokenResponse.access_token === "string"
347
+ ? tokenResponse.access_token
348
+ : undefined;
349
+ const tokenRefreshMode = refreshMode();
350
+ const refreshToken = tokenRefreshMode === "token"
351
+ ? tokenResponse.refresh_token
352
+ : undefined;
353
+ const hasZkArtifacts = !!drkJwe || !!zkDrkHash;
354
+ if (!hasZkArtifacts) {
355
+ clearCallbackUrl();
356
+ return storeSession({ idToken, accessToken, drk: EMPTY_DRK, refreshToken });
357
+ }
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
+ const privateJwkString = sessionStorage.getItem("zk_eph_priv_jwk");
366
+ if (!privateJwkString)
367
+ throw new Error("Missing ZK private key for callback");
368
+ sessionStorage.removeItem("zk_eph_priv_jwk");
369
+ const privateKey = await crypto.subtle.importKey("jwk", JSON.parse(privateJwkString), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits", "deriveKey"]);
370
+ const { plaintext } = await compactDecrypt(drkJwe, privateKey);
371
+ const drk = new Uint8Array(plaintext);
372
+ clearCallbackUrl();
373
+ return storeSession({ idToken, accessToken, drk, refreshToken });
189
374
  }
190
- const privateJwkString = sessionStorage.getItem("zk_eph_priv_jwk");
191
- if (!privateJwkString)
192
- throw new Error("Missing ZK private key for callback");
193
- sessionStorage.removeItem("zk_eph_priv_jwk");
194
- const privateKey = await crypto.subtle.importKey("jwk", JSON.parse(privateJwkString), { name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits", "deriveKey"]);
195
- const { plaintext } = await compactDecrypt(drkJwe, privateKey);
196
- const drk = new Uint8Array(plaintext);
197
- try {
198
- history.replaceState(null, "", location.origin + location.pathname);
375
+ finally {
376
+ clearCallbackStorage();
199
377
  }
200
- catch { }
201
- setStoredIdToken(idToken);
202
- const obfuscatedDrk = obfuscateKey(drk);
203
- localStorage.setItem("drk_protected", bytesToBase64Url(obfuscatedDrk));
204
- if (refreshToken)
205
- localStorage.setItem("refresh_token", refreshToken);
206
- return { idToken, drk, refreshToken };
207
378
  })();
208
379
  callbackInFlight = exchangePromise;
209
380
  callbackInFlightCode = code;
@@ -218,69 +389,99 @@ export async function handleCallback() {
218
389
  }
219
390
  }
220
391
  export function getStoredSession() {
392
+ if (memorySession) {
393
+ if (isTokenValid(memorySession.idToken))
394
+ return memorySession;
395
+ memorySession = null;
396
+ }
397
+ if (tokenStorageMode() !== "localStorage") {
398
+ clearStoredIdToken();
399
+ clearStoredAccessToken();
400
+ clearStoredDrk();
401
+ localStorage.removeItem(REFRESH_TOKEN_KEY);
402
+ return null;
403
+ }
221
404
  const idToken = getStoredIdToken();
222
- const obfuscatedDrkBase64 = localStorage.getItem("drk_protected");
405
+ const accessToken = getStoredAccessToken() || undefined;
223
406
  if (!idToken)
224
407
  return null;
225
408
  if (!isTokenValid(idToken))
226
409
  return null;
227
- if (!obfuscatedDrkBase64)
228
- return { idToken, drk: EMPTY_DRK };
229
- try {
230
- const obfuscatedDrk = base64UrlToBytes(obfuscatedDrkBase64);
231
- const drk = deobfuscateKey(obfuscatedDrk);
232
- return { idToken, drk };
233
- }
234
- catch {
235
- localStorage.removeItem("drk_protected");
236
- return null;
237
- }
410
+ return {
411
+ idToken,
412
+ accessToken,
413
+ drk: getStoredDrk() || EMPTY_DRK,
414
+ refreshToken: refreshMode() === "token" ? localStorage.getItem(REFRESH_TOKEN_KEY) || undefined : undefined,
415
+ };
238
416
  }
239
417
  export async function refreshSession() {
240
- const refreshToken = localStorage.getItem("refresh_token");
241
- if (!refreshToken)
418
+ const currentRefreshMode = refreshMode();
419
+ const refreshToken = currentRefreshMode === "token"
420
+ ? memoryRefreshToken || localStorage.getItem(REFRESH_TOKEN_KEY)
421
+ : null;
422
+ if (currentRefreshMode === "token" && !refreshToken)
242
423
  return null;
243
- const tokenUrl = new URL("/token", cfg.issuer);
424
+ const endpoints = await resolveEndpoints();
425
+ const tokenUrl = new URL(endpoints.tokenEndpoint);
426
+ const body = new URLSearchParams({
427
+ grant_type: "refresh_token",
428
+ client_id: cfg.clientId,
429
+ });
430
+ if (currentRefreshMode === "token" && refreshToken) {
431
+ body.set("refresh_token", refreshToken);
432
+ }
244
433
  const response = await fetch(tokenUrl.toString(), {
245
434
  method: "POST",
246
435
  headers: { "content-type": "application/x-www-form-urlencoded" },
247
- body: new URLSearchParams({
248
- grant_type: "refresh_token",
249
- refresh_token: refreshToken,
250
- client_id: cfg.clientId,
251
- }),
436
+ body,
437
+ credentials: fetchCredentials(),
252
438
  });
253
439
  if (!response.ok) {
254
440
  if (response.status === 401) {
255
- const latestRefreshToken = localStorage.getItem("refresh_token");
256
- if (latestRefreshToken === refreshToken) {
257
- localStorage.removeItem("refresh_token");
441
+ if (currentRefreshMode === "token") {
442
+ const latestRefreshToken = localStorage.getItem(REFRESH_TOKEN_KEY);
443
+ if (latestRefreshToken === refreshToken) {
444
+ localStorage.removeItem(REFRESH_TOKEN_KEY);
445
+ }
446
+ if (memoryRefreshToken === refreshToken) {
447
+ memoryRefreshToken = null;
448
+ }
258
449
  }
450
+ memorySession = null;
259
451
  }
260
452
  return null;
261
453
  }
262
454
  const tokenResponse = await response.json();
263
455
  const idToken = tokenResponse.id_token;
264
- const newRefreshToken = tokenResponse.refresh_token;
265
- setStoredIdToken(idToken);
266
- if (newRefreshToken)
267
- localStorage.setItem("refresh_token", newRefreshToken);
268
- const obfuscatedDrkBase64 = localStorage.getItem("drk_protected");
269
- if (!obfuscatedDrkBase64)
270
- return { idToken, drk: EMPTY_DRK, refreshToken: newRefreshToken || refreshToken };
271
- const obfuscatedDrk = base64UrlToBytes(obfuscatedDrkBase64);
272
- const drk = deobfuscateKey(obfuscatedDrk);
273
- return { idToken, drk, refreshToken: newRefreshToken || refreshToken };
456
+ const accessToken = typeof tokenResponse.access_token === "string"
457
+ ? tokenResponse.access_token
458
+ : undefined;
459
+ const newRefreshToken = currentRefreshMode === "token"
460
+ ? tokenResponse.refresh_token
461
+ : undefined;
462
+ const drk = memorySession?.drk && memorySession.drk.length > 0
463
+ ? memorySession.drk
464
+ : getStoredDrk() || EMPTY_DRK;
465
+ return storeSession({
466
+ idToken,
467
+ accessToken,
468
+ drk,
469
+ refreshToken: currentRefreshMode === "token" ? newRefreshToken || refreshToken || undefined : undefined,
470
+ });
274
471
  }
275
472
  export function logout() {
473
+ memorySession = null;
474
+ memoryRefreshToken = null;
276
475
  clearStoredIdToken();
277
- localStorage.removeItem("drk_protected");
476
+ clearStoredAccessToken();
477
+ clearStoredDrk();
278
478
  sessionStorage.removeItem("zk_eph_priv_jwk");
279
479
  sessionStorage.removeItem("pkce_verifier");
280
- localStorage.removeItem("refresh_token");
480
+ sessionStorage.removeItem(OAUTH_STATE_KEY);
481
+ localStorage.removeItem(REFRESH_TOKEN_KEY);
281
482
  }
282
483
  export function getCurrentUser() {
283
- const idToken = getStoredIdToken();
484
+ const idToken = memorySession?.idToken || (tokenStorageMode() === "localStorage" ? getStoredIdToken() : null);
284
485
  if (!idToken)
285
486
  return null;
286
487
  return parseJwt(idToken);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@darkauth/client",
3
- "version": "1.7.1",
3
+ "version": "1.8.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "directory": "packages/darkauth-client",