@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 +39 -11
- package/dist/index.d.ts +10 -0
- package/dist/index.js +295 -94
- package/package.json +1 -1
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**:
|
|
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
|
-
- **
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
- **
|
|
224
|
-
- **
|
|
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
|
|
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
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
|
405
|
+
const accessToken = getStoredAccessToken() || undefined;
|
|
223
406
|
if (!idToken)
|
|
224
407
|
return null;
|
|
225
408
|
if (!isTokenValid(idToken))
|
|
226
409
|
return null;
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
241
|
-
|
|
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
|
|
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
|
|
248
|
-
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return {
|
|
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
|
-
|
|
476
|
+
clearStoredAccessToken();
|
|
477
|
+
clearStoredDrk();
|
|
278
478
|
sessionStorage.removeItem("zk_eph_priv_jwk");
|
|
279
479
|
sessionStorage.removeItem("pkce_verifier");
|
|
280
|
-
|
|
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);
|