@pod-os/elements 0.31.1-rc.fd664af.0 → 0.32.1-rc.0fc2066.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/dist/cjs/ion-icon_31.cjs.entry.js +358 -687
- package/dist/cjs/ion-icon_31.cjs.entry.js.map +1 -1
- package/dist/components/pos-app2.js +358 -687
- package/dist/components/pos-app2.js.map +1 -1
- package/dist/elements/elements.esm.js +1 -1
- package/dist/elements/{p-2a84b1f2.entry.js → p-ed5c0c18.entry.js} +32 -32
- package/dist/elements/p-ed5c0c18.entry.js.map +1 -0
- package/dist/esm/ion-icon_31.entry.js +358 -687
- package/dist/esm/ion-icon_31.entry.js.map +1 -1
- package/package.json +3 -3
- package/dist/elements/p-2a84b1f2.entry.js.map +0 -1
|
@@ -2129,6 +2129,160 @@ const requestDynamicClientRegistration = async (registration_endpoint, client_de
|
|
|
2129
2129
|
});
|
|
2130
2130
|
};
|
|
2131
2131
|
|
|
2132
|
+
/**
|
|
2133
|
+
* A simple IndexedDB wrapper
|
|
2134
|
+
*/
|
|
2135
|
+
class SessionDatabase {
|
|
2136
|
+
dbName;
|
|
2137
|
+
storeName;
|
|
2138
|
+
dbVersion;
|
|
2139
|
+
db = null;
|
|
2140
|
+
/**
|
|
2141
|
+
* Creates a new instance
|
|
2142
|
+
* @param dbName The name of the IndexedDB database
|
|
2143
|
+
* @param storeName The name of the object store
|
|
2144
|
+
* @param dbVersion The database version
|
|
2145
|
+
*/
|
|
2146
|
+
constructor(dbName = 'soidc', storeName = 'session', dbVersion = 1) {
|
|
2147
|
+
this.dbName = dbName;
|
|
2148
|
+
this.storeName = storeName;
|
|
2149
|
+
this.dbVersion = dbVersion;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Initializes the IndexedDB database
|
|
2153
|
+
* @returns Promise that resolves when the database is ready
|
|
2154
|
+
*/
|
|
2155
|
+
async init() {
|
|
2156
|
+
return new Promise((resolve, reject) => {
|
|
2157
|
+
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
2158
|
+
request.onerror = (event) => {
|
|
2159
|
+
reject(new Error(`Database error: ${event.target.error}`));
|
|
2160
|
+
};
|
|
2161
|
+
request.onsuccess = (event) => {
|
|
2162
|
+
this.db = event.target.result;
|
|
2163
|
+
resolve(this);
|
|
2164
|
+
};
|
|
2165
|
+
request.onupgradeneeded = (event) => {
|
|
2166
|
+
const db = event.target.result;
|
|
2167
|
+
// Check if the object store already exists, if not create it
|
|
2168
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
2169
|
+
db.createObjectStore(this.storeName);
|
|
2170
|
+
}
|
|
2171
|
+
};
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Stores any value in the database with the given ID as key
|
|
2176
|
+
* @param id The identifier/key for the value
|
|
2177
|
+
* @param value The value to store
|
|
2178
|
+
*/
|
|
2179
|
+
async setItem(id, value) {
|
|
2180
|
+
if (!this.db) {
|
|
2181
|
+
await this.init();
|
|
2182
|
+
}
|
|
2183
|
+
return new Promise((resolve, reject) => {
|
|
2184
|
+
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2185
|
+
// Handle transation
|
|
2186
|
+
transaction.oncomplete = () => {
|
|
2187
|
+
resolve();
|
|
2188
|
+
};
|
|
2189
|
+
transaction.onerror = (event) => {
|
|
2190
|
+
reject(new Error(`Transaction error for setItem(${id},...): ${event.target.error}`));
|
|
2191
|
+
};
|
|
2192
|
+
transaction.onabort = (event) => {
|
|
2193
|
+
reject(new Error(`Transaction aborted for setItem(${id},...): ${event.target.error}`));
|
|
2194
|
+
};
|
|
2195
|
+
// Perform the request within the transaction
|
|
2196
|
+
const store = transaction.objectStore(this.storeName);
|
|
2197
|
+
store.put(value, id);
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
/**
|
|
2201
|
+
* Retrieves a value from the database by ID
|
|
2202
|
+
* @param id The identifier/key for the value
|
|
2203
|
+
* @returns The stored value or null if not found
|
|
2204
|
+
*/
|
|
2205
|
+
async getItem(id) {
|
|
2206
|
+
if (!this.db) {
|
|
2207
|
+
await this.init();
|
|
2208
|
+
}
|
|
2209
|
+
return new Promise((resolve, reject) => {
|
|
2210
|
+
const transaction = this.db.transaction(this.storeName, 'readonly');
|
|
2211
|
+
// Handle transation
|
|
2212
|
+
transaction.onerror = (event) => {
|
|
2213
|
+
reject(new Error(`Transaction error for getItem(${id}): ${event.target.error}`));
|
|
2214
|
+
};
|
|
2215
|
+
transaction.onabort = (event) => {
|
|
2216
|
+
reject(new Error(`Transaction aborted for getItem(${id}): ${event.target.error}`));
|
|
2217
|
+
};
|
|
2218
|
+
// Perform the request within the transaction
|
|
2219
|
+
const store = transaction.objectStore(this.storeName);
|
|
2220
|
+
const request = store.get(id);
|
|
2221
|
+
request.onsuccess = () => {
|
|
2222
|
+
resolve(request.result || null);
|
|
2223
|
+
};
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
/**
|
|
2227
|
+
* Removes an item from the database
|
|
2228
|
+
* @param id The identifier of the item to remove
|
|
2229
|
+
*/
|
|
2230
|
+
async deleteItem(id) {
|
|
2231
|
+
if (!this.db) {
|
|
2232
|
+
await this.init();
|
|
2233
|
+
}
|
|
2234
|
+
return new Promise((resolve, reject) => {
|
|
2235
|
+
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2236
|
+
// Handle transation
|
|
2237
|
+
transaction.oncomplete = () => {
|
|
2238
|
+
resolve();
|
|
2239
|
+
};
|
|
2240
|
+
transaction.onerror = (event) => {
|
|
2241
|
+
reject(new Error(`Transaction error for deleteItem(${id}): ${event.target.error}`));
|
|
2242
|
+
};
|
|
2243
|
+
transaction.onabort = (event) => {
|
|
2244
|
+
reject(new Error(`Transaction aborted for deleteItem(${id}): ${event.target.error}`));
|
|
2245
|
+
};
|
|
2246
|
+
// Perform the request within the transaction
|
|
2247
|
+
const store = transaction.objectStore(this.storeName);
|
|
2248
|
+
store.delete(id);
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
/**
|
|
2252
|
+
* Clears all items from the database
|
|
2253
|
+
*/
|
|
2254
|
+
async clear() {
|
|
2255
|
+
if (!this.db) {
|
|
2256
|
+
await this.init();
|
|
2257
|
+
}
|
|
2258
|
+
return new Promise((resolve, reject) => {
|
|
2259
|
+
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2260
|
+
// Handle transation
|
|
2261
|
+
transaction.oncomplete = () => {
|
|
2262
|
+
resolve();
|
|
2263
|
+
};
|
|
2264
|
+
transaction.onerror = (event) => {
|
|
2265
|
+
reject(new Error(`Transaction error for clear(): ${event.target.error}`));
|
|
2266
|
+
};
|
|
2267
|
+
transaction.onabort = (event) => {
|
|
2268
|
+
reject(new Error(`Transaction aborted for clear(): ${event.target.error}`));
|
|
2269
|
+
};
|
|
2270
|
+
// Perform the request within the transaction
|
|
2271
|
+
const store = transaction.objectStore(this.storeName);
|
|
2272
|
+
store.clear();
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Closes the database connection
|
|
2277
|
+
*/
|
|
2278
|
+
close() {
|
|
2279
|
+
if (this.db) {
|
|
2280
|
+
this.db.close();
|
|
2281
|
+
this.db = null;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2132
2286
|
/**
|
|
2133
2287
|
* Login with the idp, using a provided `client_id` or dynamic client registration if none provided.
|
|
2134
2288
|
*
|
|
@@ -2152,7 +2306,7 @@ const redirectForLogin = async (idp, redirect_uri, client_details) => {
|
|
|
2152
2306
|
const issuer = openid_configuration["issuer"];
|
|
2153
2307
|
const trim_trailing_slash = (url) => (url.endsWith('/') ? url.slice(0, -1) : url);
|
|
2154
2308
|
if (trim_trailing_slash(idp) !== trim_trailing_slash(issuer)) { // expected idp matches received issuer mod trailing slash?
|
|
2155
|
-
throw new Error("RFC 9207 - iss
|
|
2309
|
+
throw new Error("RFC 9207 - iss != idp - " + issuer + " != " + idp);
|
|
2156
2310
|
}
|
|
2157
2311
|
sessionStorage.setItem("idp", issuer);
|
|
2158
2312
|
// remember token endpoint
|
|
@@ -2173,12 +2327,7 @@ const redirectForLogin = async (idp, redirect_uri, client_details) => {
|
|
|
2173
2327
|
return response.json();
|
|
2174
2328
|
});
|
|
2175
2329
|
client_id = client_registration["client_id"];
|
|
2176
|
-
|
|
2177
|
-
// remember client_id if not URL
|
|
2178
|
-
try {
|
|
2179
|
-
new URL(client_id);
|
|
2180
|
-
}
|
|
2181
|
-
catch {
|
|
2330
|
+
// remember client_id
|
|
2182
2331
|
sessionStorage.setItem("client_id", client_id);
|
|
2183
2332
|
}
|
|
2184
2333
|
// RFC 7636 PKCE, remember code verifer
|
|
@@ -2219,7 +2368,7 @@ const getPKCEcode = async () => {
|
|
|
2219
2368
|
* URL contains authrization code, issuer (idp) and state (csrf token),
|
|
2220
2369
|
* get an access token for the authrization code.
|
|
2221
2370
|
*/
|
|
2222
|
-
const onIncomingRedirect = async (client_details
|
|
2371
|
+
const onIncomingRedirect = async (client_details) => {
|
|
2223
2372
|
const url = new URL(window.location.href);
|
|
2224
2373
|
// authorization code
|
|
2225
2374
|
const authorization_code = url.searchParams.get("code");
|
|
@@ -2229,12 +2378,12 @@ const onIncomingRedirect = async (client_details, database) => {
|
|
|
2229
2378
|
}
|
|
2230
2379
|
// RFC 9207 issuer check
|
|
2231
2380
|
const idp = sessionStorage.getItem("idp");
|
|
2232
|
-
if (idp === null || url.searchParams.get("iss")
|
|
2233
|
-
throw new Error("RFC 9207 - iss
|
|
2381
|
+
if (idp === null || url.searchParams.get("iss") != idp) {
|
|
2382
|
+
throw new Error("RFC 9207 - iss != idp - " + url.searchParams.get("iss") + " != " + idp);
|
|
2234
2383
|
}
|
|
2235
2384
|
// RFC 6749 OAuth 2.0
|
|
2236
|
-
if (url.searchParams.get("state")
|
|
2237
|
-
throw new Error("RFC 6749 - state
|
|
2385
|
+
if (url.searchParams.get("state") != sessionStorage.getItem("csrf_token")) {
|
|
2386
|
+
throw new Error("RFC 6749 - state != csrf_token - " + url.searchParams.get("state") + " != " + sessionStorage.getItem("csrf_token"));
|
|
2238
2387
|
}
|
|
2239
2388
|
// remove redirect query parameters from URL
|
|
2240
2389
|
url.searchParams.delete("iss");
|
|
@@ -2278,32 +2427,30 @@ const onIncomingRedirect = async (client_details, database) => {
|
|
|
2278
2427
|
});
|
|
2279
2428
|
// check dpop thumbprint
|
|
2280
2429
|
const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
|
|
2281
|
-
if (payload["cnf"]["jkt"]
|
|
2282
|
-
throw new Error("Access Token validation failed on `jkt`: jkt
|
|
2430
|
+
if (payload["cnf"]["jkt"] != dpopThumbprint) {
|
|
2431
|
+
throw new Error("Access Token validation failed on `jkt`: jkt != DPoP thumbprint - " + payload["cnf"]["jkt"] + " != " + dpopThumbprint);
|
|
2283
2432
|
}
|
|
2284
2433
|
// check client_id
|
|
2285
|
-
if (payload["client_id"]
|
|
2286
|
-
throw new Error("Access Token validation failed on `client_id`: JWT payload
|
|
2434
|
+
if (payload["client_id"] != client_id) {
|
|
2435
|
+
throw new Error("Access Token validation failed on `client_id`: JWT payload != client_id - " + payload["client_id"] + " != " + client_id);
|
|
2287
2436
|
}
|
|
2288
2437
|
// summarise session info
|
|
2289
2438
|
const token_details = { ...token_response, dpop_key_pair: key_pair };
|
|
2290
2439
|
const idp_details = { idp, jwks_uri, token_endpoint };
|
|
2291
2440
|
if (!client_details)
|
|
2292
|
-
client_details = { redirect_uris: [
|
|
2441
|
+
client_details = { redirect_uris: [window.location.href] };
|
|
2293
2442
|
client_details.client_id = client_id;
|
|
2294
|
-
//
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
database.close();
|
|
2306
|
-
}
|
|
2443
|
+
// to remember for session restore
|
|
2444
|
+
const sessionDatabase = await new SessionDatabase().init();
|
|
2445
|
+
await Promise.all([
|
|
2446
|
+
sessionDatabase.setItem("idp", idp),
|
|
2447
|
+
sessionDatabase.setItem("jwks_uri", jwks_uri),
|
|
2448
|
+
sessionDatabase.setItem("token_endpoint", token_endpoint),
|
|
2449
|
+
sessionDatabase.setItem("client_id", client_id),
|
|
2450
|
+
sessionDatabase.setItem("dpop_keypair", key_pair),
|
|
2451
|
+
sessionDatabase.setItem("refresh_token", token_response["refresh_token"])
|
|
2452
|
+
]);
|
|
2453
|
+
sessionDatabase.close();
|
|
2307
2454
|
// clean session storage
|
|
2308
2455
|
sessionStorage.removeItem("csrf_token");
|
|
2309
2456
|
sessionStorage.removeItem("pkce_code_verifier");
|
|
@@ -2361,60 +2508,56 @@ const requestAccessToken = async (authorization_code, pkce_code_verifier, redire
|
|
|
2361
2508
|
});
|
|
2362
2509
|
};
|
|
2363
2510
|
|
|
2364
|
-
const renewTokens = async (
|
|
2511
|
+
const renewTokens = async () => {
|
|
2365
2512
|
// remember session details
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
2380
|
-
}
|
|
2381
|
-
return response.json();
|
|
2382
|
-
});
|
|
2383
|
-
// verify access_token // ! Solid-OIDC specification says it should be a dpop-bound `id token` but implementations provide a dpop-bound `access token`
|
|
2384
|
-
const accessToken = token_response["access_token"];
|
|
2385
|
-
const idp = await sessionDatabase.getItem("idp");
|
|
2386
|
-
if (idp === null) {
|
|
2387
|
-
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: idp");
|
|
2388
|
-
}
|
|
2389
|
-
const jwks_uri = await sessionDatabase.getItem("jwks_uri");
|
|
2390
|
-
if (jwks_uri === null) {
|
|
2391
|
-
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: jwks_uri");
|
|
2392
|
-
}
|
|
2393
|
-
const jwks = createRemoteJWKSet(new URL(jwks_uri));
|
|
2394
|
-
const { payload } = await jwtVerify(accessToken, jwks, {
|
|
2395
|
-
issuer: idp, // RFC 9207
|
|
2396
|
-
audience: "solid", // RFC 7519 // ! "solid" as per implementations ...
|
|
2397
|
-
// exp, nbf, iat - handled automatically
|
|
2398
|
-
});
|
|
2399
|
-
// check dpop thumbprint
|
|
2400
|
-
const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
|
|
2401
|
-
if (payload["cnf"]["jkt"] !== dpopThumbprint) {
|
|
2402
|
-
throw new Error("Access Token validation failed on `jkt`: jkt !== DPoP thumbprint - " + payload["cnf"]["jkt"] + " !== " + dpopThumbprint);
|
|
2403
|
-
}
|
|
2404
|
-
// check client_id
|
|
2405
|
-
if (payload["client_id"] !== client_id) {
|
|
2406
|
-
throw new Error("Access Token validation failed on `client_id`: JWT payload !== client_id - " + payload["client_id"] + " !== " + client_id);
|
|
2513
|
+
const sessionDatabase = await new SessionDatabase().init();
|
|
2514
|
+
const client_id = await sessionDatabase.getItem("client_id");
|
|
2515
|
+
const token_endpoint = await sessionDatabase.getItem("token_endpoint");
|
|
2516
|
+
const key_pair = await sessionDatabase.getItem("dpop_keypair");
|
|
2517
|
+
const refresh_token = await sessionDatabase.getItem("refresh_token");
|
|
2518
|
+
if (client_id === null || token_endpoint === null || key_pair === null || refresh_token === null) {
|
|
2519
|
+
// we can not restore the old session
|
|
2520
|
+
throw new Error("Could not refresh tokens: details missing from database.");
|
|
2521
|
+
}
|
|
2522
|
+
const token_response = await requestFreshTokens(refresh_token, client_id, token_endpoint, key_pair)
|
|
2523
|
+
.then((response) => {
|
|
2524
|
+
if (!response.ok) {
|
|
2525
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
2407
2526
|
}
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2527
|
+
return response.json();
|
|
2528
|
+
});
|
|
2529
|
+
// verify access_token // ! Solid-OIDC specification says it should be a dpop-bound `id token` but implementations provide a dpop-bound `access token`
|
|
2530
|
+
const accessToken = token_response["access_token"];
|
|
2531
|
+
const idp = await sessionDatabase.getItem("idp");
|
|
2532
|
+
if (idp === null) {
|
|
2533
|
+
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: idp");
|
|
2414
2534
|
}
|
|
2415
|
-
|
|
2416
|
-
|
|
2535
|
+
const jwks_uri = await sessionDatabase.getItem("jwks_uri");
|
|
2536
|
+
if (jwks_uri === null) {
|
|
2537
|
+
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: jwks_uri");
|
|
2538
|
+
}
|
|
2539
|
+
const jwks = createRemoteJWKSet(new URL(jwks_uri));
|
|
2540
|
+
const { payload } = await jwtVerify(accessToken, jwks, {
|
|
2541
|
+
issuer: idp, // RFC 9207
|
|
2542
|
+
audience: "solid", // RFC 7519 // ! "solid" as per implementations ...
|
|
2543
|
+
// exp, nbf, iat - handled automatically
|
|
2544
|
+
});
|
|
2545
|
+
// check dpop thumbprint
|
|
2546
|
+
const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
|
|
2547
|
+
if (payload["cnf"]["jkt"] != dpopThumbprint) {
|
|
2548
|
+
throw new Error("Access Token validation failed on `jkt`: jkt != DPoP thumbprint - " + payload["cnf"]["jkt"] + " != " + dpopThumbprint);
|
|
2549
|
+
}
|
|
2550
|
+
// check client_id
|
|
2551
|
+
if (payload["client_id"] != client_id) {
|
|
2552
|
+
throw new Error("Access Token validation failed on `client_id`: JWT payload != client_id - " + payload["client_id"] + " != " + client_id);
|
|
2417
2553
|
}
|
|
2554
|
+
// set new refresh token for token rotation
|
|
2555
|
+
await sessionDatabase.setItem("refresh_token", token_response["refresh_token"]);
|
|
2556
|
+
sessionDatabase.close();
|
|
2557
|
+
return {
|
|
2558
|
+
...token_response,
|
|
2559
|
+
dpop_key_pair: key_pair,
|
|
2560
|
+
};
|
|
2418
2561
|
};
|
|
2419
2562
|
/**
|
|
2420
2563
|
* Request an dpop-bound access token from a token endpoint using a refresh token
|
|
@@ -2436,7 +2579,7 @@ const requestFreshTokens = async (refresh_token, client_id, token_endpoint, key_
|
|
|
2436
2579
|
htm: "POST",
|
|
2437
2580
|
})
|
|
2438
2581
|
.setIssuedAt()
|
|
2439
|
-
.setJti(
|
|
2582
|
+
.setJti(window.crypto.randomUUID())
|
|
2440
2583
|
.setProtectedHeader({
|
|
2441
2584
|
alg: "ES256",
|
|
2442
2585
|
typ: "dpop+jwt",
|
|
@@ -2457,118 +2600,113 @@ const requestFreshTokens = async (refresh_token, client_id, token_endpoint, key_
|
|
|
2457
2600
|
});
|
|
2458
2601
|
};
|
|
2459
2602
|
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
// Basic implementation
|
|
2463
|
-
//
|
|
2464
|
-
//
|
|
2465
|
-
/**
|
|
2466
|
-
* The SessionCore class manages session state and core logic but does not handle the refresh lifecycle.
|
|
2467
|
-
* It receives {@link SessionOptions} with a database to be able to restore a session.
|
|
2468
|
-
* That database can be re-used by (your!) surrounding implementation to handle the refresh lifecycle.
|
|
2469
|
-
* If no database was provided, refresh information cannot be stored, and thus token refresh (via the refresh token grant) is not possible in this case.
|
|
2470
|
-
*
|
|
2471
|
-
* If you are building a web app, use the Session implementation provided in the default `/web` version of this library.
|
|
2472
|
-
*/
|
|
2473
|
-
class SessionCore {
|
|
2603
|
+
class Session {
|
|
2604
|
+
sessionInformation;
|
|
2474
2605
|
isActive_ = false;
|
|
2475
|
-
exp_;
|
|
2476
2606
|
webId_ = undefined;
|
|
2477
2607
|
currentAth_ = undefined;
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2608
|
+
tokenRefreshTimeout;
|
|
2609
|
+
sessionDeactivateTimeout;
|
|
2610
|
+
onSessionExpirationWarning;
|
|
2611
|
+
/**
|
|
2612
|
+
* Create a new session.
|
|
2613
|
+
*/
|
|
2484
2614
|
constructor(clientDetails, sessionOptions) {
|
|
2485
|
-
this.
|
|
2486
|
-
this.
|
|
2487
|
-
this.onSessionStateChange = sessionOptions?.onSessionStateChange;
|
|
2615
|
+
this.sessionInformation = { clientDetails };
|
|
2616
|
+
this.onSessionExpirationWarning = sessionOptions?.onSessionExpirationWarning;
|
|
2488
2617
|
}
|
|
2618
|
+
/**
|
|
2619
|
+
* Redirect the user for login to their IDP.
|
|
2620
|
+
*
|
|
2621
|
+
* @throws Error if the session has not been initialized.
|
|
2622
|
+
*/
|
|
2489
2623
|
async login(idp, redirect_uri) {
|
|
2490
|
-
await redirectForLogin(idp, redirect_uri, this.
|
|
2624
|
+
await redirectForLogin(idp, redirect_uri, this.sessionInformation.clientDetails);
|
|
2625
|
+
}
|
|
2626
|
+
/**
|
|
2627
|
+
* Clears all session-related information, including IDP details and tokens.
|
|
2628
|
+
* This logs the user out.
|
|
2629
|
+
* Client details are preserved.
|
|
2630
|
+
*/
|
|
2631
|
+
async logout() {
|
|
2632
|
+
// clear timeouts
|
|
2633
|
+
if (this.sessionDeactivateTimeout)
|
|
2634
|
+
clearTimeout(this.sessionDeactivateTimeout);
|
|
2635
|
+
this.sessionDeactivateTimeout = undefined;
|
|
2636
|
+
if (this.tokenRefreshTimeout)
|
|
2637
|
+
clearTimeout(this.tokenRefreshTimeout);
|
|
2638
|
+
this.tokenRefreshTimeout = undefined;
|
|
2639
|
+
// clean session data
|
|
2640
|
+
this.sessionInformation.idpDetails = undefined;
|
|
2641
|
+
this.sessionInformation.tokenDetails = undefined;
|
|
2642
|
+
this.isActive_ = false;
|
|
2643
|
+
this.webId_ = undefined;
|
|
2644
|
+
// only preserve client_id if URI
|
|
2645
|
+
if (this.sessionInformation.clientDetails?.client_id)
|
|
2646
|
+
try {
|
|
2647
|
+
new URL(this.sessionInformation.clientDetails.client_id);
|
|
2648
|
+
}
|
|
2649
|
+
catch (_) {
|
|
2650
|
+
this.sessionInformation.clientDetails.client_id = undefined;
|
|
2651
|
+
}
|
|
2652
|
+
// clean session database
|
|
2653
|
+
const sessionDatabase = await new SessionDatabase().init();
|
|
2654
|
+
await sessionDatabase.clear();
|
|
2655
|
+
sessionDatabase.close();
|
|
2491
2656
|
}
|
|
2492
2657
|
/**
|
|
2493
2658
|
* Handles the redirect from the identity provider after a login attempt.
|
|
2494
2659
|
* It attempts to retrieve tokens using the authorization code.
|
|
2495
|
-
* Upon success, it tries to persist information to refresh tokens in the session database.
|
|
2496
|
-
* If no database was provided, no information is persisted.
|
|
2497
2660
|
*/
|
|
2498
2661
|
async handleRedirectFromLogin() {
|
|
2499
2662
|
// Redirect after Authorization Code Grant // memory via sessionStorage
|
|
2500
|
-
const newSessionInfo = await onIncomingRedirect(this.
|
|
2663
|
+
const newSessionInfo = await onIncomingRedirect(this.sessionInformation.clientDetails);
|
|
2501
2664
|
// no session - we remain unauthenticated
|
|
2502
|
-
if (!newSessionInfo.tokenDetails)
|
|
2665
|
+
if (!newSessionInfo.tokenDetails) {
|
|
2503
2666
|
return;
|
|
2667
|
+
}
|
|
2504
2668
|
// we got a session
|
|
2505
|
-
this.
|
|
2506
|
-
this.
|
|
2507
|
-
await this.setTokenDetails(newSessionInfo.tokenDetails);
|
|
2508
|
-
// callback state change
|
|
2509
|
-
this.onSessionStateChange?.(); // we logged in
|
|
2669
|
+
this.sessionInformation = newSessionInfo;
|
|
2670
|
+
await this.setSessionDetails();
|
|
2510
2671
|
}
|
|
2511
2672
|
/**
|
|
2512
2673
|
* Handles session restoration using the refresh token grant.
|
|
2513
2674
|
* Silently fails if session could not be restored (maybe there was no session in the first place).
|
|
2514
2675
|
*/
|
|
2515
2676
|
async restore() {
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
// Restore session using Refresh Token Grant
|
|
2527
|
-
const wasActive = this.isActive;
|
|
2528
|
-
renewTokens(this.database)
|
|
2529
|
-
.then(tokenDetails => this.setTokenDetails(tokenDetails))
|
|
2530
|
-
.then(() => this.resolveRefresh())
|
|
2531
|
-
.catch(error => {
|
|
2532
|
-
if (this.isActive) {
|
|
2533
|
-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
|
|
2534
|
-
// do not change state (yet), let the app decide if they want to logout or if they just want to retry.
|
|
2535
|
-
}
|
|
2536
|
-
else {
|
|
2537
|
-
this.rejectRefresh(new Error("No session to restore."));
|
|
2538
|
-
}
|
|
2539
|
-
}).finally(() => {
|
|
2540
|
-
this.clearRefreshPromise();
|
|
2541
|
-
if (wasActive !== this.isActive)
|
|
2542
|
-
this.onSessionStateChange?.();
|
|
2543
|
-
});
|
|
2544
|
-
return this.refreshPromise;
|
|
2677
|
+
// Restore session using Refresh Token Grant // memory via IndexedDB
|
|
2678
|
+
await renewTokens()
|
|
2679
|
+
.then(tokenDetails => {
|
|
2680
|
+
// got new tokens
|
|
2681
|
+
this.sessionInformation.tokenDetails = tokenDetails;
|
|
2682
|
+
// set session information
|
|
2683
|
+
return this.setSessionDetails();
|
|
2684
|
+
})
|
|
2685
|
+
// anything missing or wrong => abort, could not restore session.
|
|
2686
|
+
.catch(_ => { }); // fail silently
|
|
2545
2687
|
}
|
|
2546
2688
|
/**
|
|
2547
|
-
*
|
|
2548
|
-
*
|
|
2549
|
-
*
|
|
2689
|
+
* Creates a signed DPoP (Demonstration of Proof-of-Possession) token.
|
|
2690
|
+
*
|
|
2691
|
+
* @param payload The payload to include in the DPoP token. By default, it includes `htu` (HTTP target URI) and `htm` (HTTP method).
|
|
2692
|
+
* @returns A promise that resolves to the signed DPoP token string.
|
|
2693
|
+
* @throws Error if the session has not been initialized - if no token details are available.
|
|
2550
2694
|
*/
|
|
2551
|
-
async
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
this.exp_ = undefined;
|
|
2555
|
-
this.webId_ = undefined;
|
|
2556
|
-
this.currentAth_ = undefined;
|
|
2557
|
-
this.information.idpDetails = undefined;
|
|
2558
|
-
this.information.tokenDetails = undefined;
|
|
2559
|
-
// client details are preserved
|
|
2560
|
-
if (this.refreshPromise && this.rejectRefresh) {
|
|
2561
|
-
this.rejectRefresh(new Error('Logout during token refresh.'));
|
|
2562
|
-
this.clearRefreshPromise();
|
|
2563
|
-
}
|
|
2564
|
-
// clean session database
|
|
2565
|
-
if (this.database) {
|
|
2566
|
-
await this.database.init();
|
|
2567
|
-
await this.database.clear();
|
|
2568
|
-
this.database.close();
|
|
2695
|
+
async createSignedDPoPToken(payload) {
|
|
2696
|
+
if (!this.sessionInformation.tokenDetails || !this.currentAth_) {
|
|
2697
|
+
throw new Error("Session not established.");
|
|
2569
2698
|
}
|
|
2570
|
-
|
|
2571
|
-
this.
|
|
2699
|
+
payload.ath = this.currentAth_;
|
|
2700
|
+
const jwk_public_key = await exportJWK(this.sessionInformation.tokenDetails.dpop_key_pair.publicKey);
|
|
2701
|
+
return new SignJWT(payload)
|
|
2702
|
+
.setIssuedAt()
|
|
2703
|
+
.setJti(window.crypto.randomUUID())
|
|
2704
|
+
.setProtectedHeader({
|
|
2705
|
+
alg: "ES256",
|
|
2706
|
+
typ: "dpop+jwt",
|
|
2707
|
+
jwk: jwk_public_key,
|
|
2708
|
+
})
|
|
2709
|
+
.sign(this.sessionInformation.tokenDetails.dpop_key_pair.privateKey);
|
|
2572
2710
|
}
|
|
2573
2711
|
/**
|
|
2574
2712
|
* Makes an HTTP fetch request.
|
|
@@ -2580,18 +2718,10 @@ class SessionCore {
|
|
|
2580
2718
|
* @returns A promise that resolves to the fetch Response.
|
|
2581
2719
|
*/
|
|
2582
2720
|
async authFetch(input, init, dpopPayload) {
|
|
2583
|
-
// if there is not session established, just delegate to the default fetch
|
|
2584
|
-
if (!this.isActive) {
|
|
2585
|
-
return fetch(input, init);
|
|
2586
|
-
}
|
|
2587
|
-
// TODO
|
|
2588
|
-
// TODO do HEAD request to check if authentication is actually required, only then include tokens
|
|
2589
|
-
// TODO
|
|
2590
2721
|
// prepare authenticated call using a DPoP token (either provided payload, or default)
|
|
2591
2722
|
let url;
|
|
2592
2723
|
let method;
|
|
2593
2724
|
let headers;
|
|
2594
|
-
// wrangle fetch input parameters into place
|
|
2595
2725
|
if (input instanceof Request) {
|
|
2596
2726
|
url = new URL(input.url);
|
|
2597
2727
|
method = init?.method || input?.method || 'GET';
|
|
@@ -2604,15 +2734,15 @@ class SessionCore {
|
|
|
2604
2734
|
headers = init.headers ? new Headers(init.headers) : new Headers();
|
|
2605
2735
|
}
|
|
2606
2736
|
// create DPoP token, and add tokens to request
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2737
|
+
if (this.sessionInformation.tokenDetails) {
|
|
2738
|
+
dpopPayload = dpopPayload ?? {
|
|
2739
|
+
htu: `${url.origin}${url.pathname}`,
|
|
2740
|
+
htm: method.toUpperCase()
|
|
2741
|
+
};
|
|
2742
|
+
const dpop = await this.createSignedDPoPToken(dpopPayload);
|
|
2743
|
+
headers.set("dpop", dpop);
|
|
2744
|
+
headers.set("authorization", `DPoP ${this.sessionInformation.tokenDetails.access_token}`);
|
|
2745
|
+
}
|
|
2616
2746
|
// check explicitly; to avoid unexpected behaviour
|
|
2617
2747
|
if (input instanceof Request) { // clone the provided request, and override the headers
|
|
2618
2748
|
return fetch(new Request(input, { ...init, headers }));
|
|
@@ -2620,59 +2750,64 @@ class SessionCore {
|
|
|
2620
2750
|
// just override the headers
|
|
2621
2751
|
return fetch(url, { ...init, headers });
|
|
2622
2752
|
}
|
|
2623
|
-
//
|
|
2624
|
-
// Setters
|
|
2625
|
-
//
|
|
2626
|
-
async setTokenDetails(tokenDetails) {
|
|
2627
|
-
this.information.tokenDetails = tokenDetails;
|
|
2628
|
-
await this._updateSessionDetailsFromToken(tokenDetails.access_token);
|
|
2629
|
-
}
|
|
2630
|
-
clearRefreshPromise() {
|
|
2631
|
-
this.refreshPromise = undefined;
|
|
2632
|
-
this.resolveRefresh = undefined;
|
|
2633
|
-
this.rejectRefresh = undefined;
|
|
2634
|
-
}
|
|
2635
|
-
//
|
|
2636
|
-
// Getters
|
|
2637
|
-
//
|
|
2638
2753
|
get isActive() {
|
|
2639
2754
|
return this.isActive_;
|
|
2640
2755
|
}
|
|
2641
2756
|
get webId() {
|
|
2642
2757
|
return this.webId_;
|
|
2643
2758
|
}
|
|
2644
|
-
getExpiresIn() {
|
|
2645
|
-
return this.information.tokenDetails?.expires_in ?? -1;
|
|
2646
|
-
}
|
|
2647
|
-
isExpired() {
|
|
2648
|
-
if (!this.exp_)
|
|
2649
|
-
return true;
|
|
2650
|
-
return this._isTokenExpired(this.exp_);
|
|
2651
|
-
}
|
|
2652
|
-
getTokenDetails() {
|
|
2653
|
-
return this.information.tokenDetails;
|
|
2654
|
-
}
|
|
2655
2759
|
//
|
|
2656
|
-
//
|
|
2760
|
+
// Helper Methods
|
|
2657
2761
|
//
|
|
2658
2762
|
/**
|
|
2659
|
-
*
|
|
2660
|
-
* and if expired, restore the session.
|
|
2763
|
+
* Set the session to active if there is an access token.
|
|
2661
2764
|
*/
|
|
2662
|
-
async
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2765
|
+
async setSessionDetails() {
|
|
2766
|
+
// check for access token
|
|
2767
|
+
if (!this.sessionInformation.tokenDetails?.access_token) {
|
|
2768
|
+
this.logout();
|
|
2769
|
+
}
|
|
2770
|
+
// generate ath
|
|
2771
|
+
this.currentAth_ = await this.computeAth(this.sessionInformation.tokenDetails.access_token);
|
|
2772
|
+
// check for active session
|
|
2773
|
+
this.webId_ = decodeJwt(this.sessionInformation.tokenDetails.access_token)["webid"];
|
|
2774
|
+
this.isActive_ = this.webId !== undefined;
|
|
2775
|
+
// deactivating session when token expire
|
|
2776
|
+
this.setSessionDeactivateTimeout();
|
|
2777
|
+
// refreshing tokens
|
|
2778
|
+
this.setTokenRefreshTimeout();
|
|
2779
|
+
}
|
|
2780
|
+
setSessionDeactivateTimeout() {
|
|
2781
|
+
const deactivate_buffer_seconds = 5;
|
|
2782
|
+
const timeUntilDeactivate = (this.sessionInformation.tokenDetails.expires_in - deactivate_buffer_seconds) * 1000;
|
|
2783
|
+
if (this.sessionDeactivateTimeout)
|
|
2784
|
+
clearTimeout(this.sessionDeactivateTimeout);
|
|
2785
|
+
this.sessionDeactivateTimeout = setTimeout(() => this.logout(), timeUntilDeactivate);
|
|
2786
|
+
}
|
|
2787
|
+
setTokenRefreshTimeout() {
|
|
2788
|
+
const refresh_buffer_seconds = 95;
|
|
2789
|
+
const timeUntilRefresh = (this.sessionInformation.tokenDetails.expires_in - refresh_buffer_seconds) * 1000;
|
|
2790
|
+
if (this.tokenRefreshTimeout)
|
|
2791
|
+
clearTimeout(this.tokenRefreshTimeout);
|
|
2792
|
+
this.tokenRefreshTimeout = setTimeout(async () => {
|
|
2793
|
+
const newTokens = await renewTokens()
|
|
2794
|
+
.catch((error) => {
|
|
2795
|
+
// anything missing or wrong => could not renew tokens.
|
|
2796
|
+
if (this.onSessionExpirationWarning)
|
|
2797
|
+
this.onSessionExpirationWarning();
|
|
2798
|
+
return undefined;
|
|
2799
|
+
});
|
|
2800
|
+
if (!newTokens) {
|
|
2801
|
+
return;
|
|
2669
2802
|
}
|
|
2670
|
-
|
|
2803
|
+
this.sessionInformation.tokenDetails = newTokens;
|
|
2804
|
+
this.setSessionDetails();
|
|
2805
|
+
}, timeUntilRefresh);
|
|
2671
2806
|
}
|
|
2672
2807
|
/**
|
|
2673
2808
|
* RFC 9449 - Hash of the access token
|
|
2674
2809
|
*/
|
|
2675
|
-
async
|
|
2810
|
+
async computeAth(accessToken) {
|
|
2676
2811
|
// Convert the ASCII string of the token to a Uint8Array
|
|
2677
2812
|
const encoder = new TextEncoder();
|
|
2678
2813
|
const data = encoder.encode(accessToken); // ASCII by default
|
|
@@ -2688,470 +2823,6 @@ class SessionCore {
|
|
|
2688
2823
|
.replace(/=+$/, '');
|
|
2689
2824
|
return base64url;
|
|
2690
2825
|
}
|
|
2691
|
-
/**
|
|
2692
|
-
* Creates a signed DPoP (Demonstration of Proof-of-Possession) token.
|
|
2693
|
-
*
|
|
2694
|
-
* @param payload The payload to include in the DPoP token. By default, it includes `htu` (HTTP target URI) and `htm` (HTTP method).
|
|
2695
|
-
* @returns A promise that resolves to the signed DPoP token string.
|
|
2696
|
-
* @throws Error if the session has not been initialized - if no token details are available.
|
|
2697
|
-
*/
|
|
2698
|
-
async _createSignedDPoPToken(payload) {
|
|
2699
|
-
if (!this.information.tokenDetails || !this.currentAth_) {
|
|
2700
|
-
throw new Error("Session not established.");
|
|
2701
|
-
}
|
|
2702
|
-
payload.ath = this.currentAth_;
|
|
2703
|
-
const jwk_public_key = await exportJWK(this.information.tokenDetails.dpop_key_pair.publicKey);
|
|
2704
|
-
return new SignJWT(payload)
|
|
2705
|
-
.setIssuedAt()
|
|
2706
|
-
.setJti(window.crypto.randomUUID())
|
|
2707
|
-
.setProtectedHeader({
|
|
2708
|
-
alg: "ES256",
|
|
2709
|
-
typ: "dpop+jwt",
|
|
2710
|
-
jwk: jwk_public_key,
|
|
2711
|
-
})
|
|
2712
|
-
.sign(this.information.tokenDetails.dpop_key_pair.privateKey);
|
|
2713
|
-
}
|
|
2714
|
-
async _updateSessionDetailsFromToken(access_token) {
|
|
2715
|
-
if (!access_token) {
|
|
2716
|
-
await this.logout();
|
|
2717
|
-
return;
|
|
2718
|
-
}
|
|
2719
|
-
try {
|
|
2720
|
-
const decodedToken = decodeJwt(access_token);
|
|
2721
|
-
const webId = decodedToken.webid;
|
|
2722
|
-
if (!webId) {
|
|
2723
|
-
throw new Error('Missing webid claim in access token');
|
|
2724
|
-
}
|
|
2725
|
-
const exp = decodedToken.exp;
|
|
2726
|
-
if (!exp) {
|
|
2727
|
-
throw new Error('Missing exp claim in access token');
|
|
2728
|
-
}
|
|
2729
|
-
this.currentAth_ = await this._computeAth(access_token); // must be done before session set to active
|
|
2730
|
-
this.webId_ = webId;
|
|
2731
|
-
this.exp_ = exp;
|
|
2732
|
-
this.isActive_ = true;
|
|
2733
|
-
}
|
|
2734
|
-
catch (error) {
|
|
2735
|
-
await this.logout();
|
|
2736
|
-
}
|
|
2737
|
-
}
|
|
2738
|
-
/**
|
|
2739
|
-
* Checks if a JWT expiration timestamp ('exp') has passed.
|
|
2740
|
-
*/
|
|
2741
|
-
_isTokenExpired(exp, bufferSeconds = 0) {
|
|
2742
|
-
if (typeof exp !== 'number' || isNaN(exp)) {
|
|
2743
|
-
return true;
|
|
2744
|
-
}
|
|
2745
|
-
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
|
2746
|
-
return exp < (currentTimeSeconds + bufferSeconds);
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
|
|
2750
|
-
// extracting this from Session.ts such that the jest tests would compile :)
|
|
2751
|
-
const getWorkerUrl = () => new URL('./RefreshWorker.js', import.meta.url);
|
|
2752
|
-
|
|
2753
|
-
/**
|
|
2754
|
-
* A simple IndexedDB wrapper.
|
|
2755
|
-
*/
|
|
2756
|
-
class SessionIDB {
|
|
2757
|
-
dbName;
|
|
2758
|
-
storeName;
|
|
2759
|
-
dbVersion;
|
|
2760
|
-
db = null;
|
|
2761
|
-
/**
|
|
2762
|
-
* Creates a new instance
|
|
2763
|
-
* @param dbName The name of the IndexedDB database
|
|
2764
|
-
* @param storeName The name of the object store
|
|
2765
|
-
* @param dbVersion The database version
|
|
2766
|
-
*/
|
|
2767
|
-
constructor(dbName = 'soidc', storeName = 'session', dbVersion = 1) {
|
|
2768
|
-
this.dbName = dbName;
|
|
2769
|
-
this.storeName = storeName;
|
|
2770
|
-
this.dbVersion = dbVersion;
|
|
2771
|
-
}
|
|
2772
|
-
/**
|
|
2773
|
-
* Initializes the IndexedDB database
|
|
2774
|
-
* @returns Promise that resolves when the database is ready
|
|
2775
|
-
*/
|
|
2776
|
-
async init() {
|
|
2777
|
-
return new Promise((resolve, reject) => {
|
|
2778
|
-
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
2779
|
-
request.onerror = (event) => {
|
|
2780
|
-
reject(new Error(`Database error: ${event.target.error}`));
|
|
2781
|
-
};
|
|
2782
|
-
request.onsuccess = (event) => {
|
|
2783
|
-
this.db = event.target.result;
|
|
2784
|
-
resolve(this);
|
|
2785
|
-
};
|
|
2786
|
-
request.onupgradeneeded = (event) => {
|
|
2787
|
-
const db = event.target.result;
|
|
2788
|
-
// Check if the object store already exists, if not create it
|
|
2789
|
-
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
2790
|
-
db.createObjectStore(this.storeName);
|
|
2791
|
-
}
|
|
2792
|
-
};
|
|
2793
|
-
});
|
|
2794
|
-
}
|
|
2795
|
-
/**
|
|
2796
|
-
* Stores any value in the database with the given ID as key
|
|
2797
|
-
* @param id The identifier/key for the value
|
|
2798
|
-
* @param value The value to store
|
|
2799
|
-
*/
|
|
2800
|
-
async setItem(id, value) {
|
|
2801
|
-
if (!this.db) {
|
|
2802
|
-
await this.init();
|
|
2803
|
-
}
|
|
2804
|
-
return new Promise((resolve, reject) => {
|
|
2805
|
-
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2806
|
-
// Handle transation
|
|
2807
|
-
transaction.oncomplete = () => {
|
|
2808
|
-
resolve();
|
|
2809
|
-
};
|
|
2810
|
-
transaction.onerror = (event) => {
|
|
2811
|
-
reject(new Error(`Transaction error for setItem(${id},...): ${event.target.error}`));
|
|
2812
|
-
};
|
|
2813
|
-
transaction.onabort = (event) => {
|
|
2814
|
-
reject(new Error(`Transaction aborted for setItem(${id},...): ${event.target.error}`));
|
|
2815
|
-
};
|
|
2816
|
-
// Perform the request within the transaction
|
|
2817
|
-
const store = transaction.objectStore(this.storeName);
|
|
2818
|
-
store.put(value, id);
|
|
2819
|
-
});
|
|
2820
|
-
}
|
|
2821
|
-
/**
|
|
2822
|
-
* Retrieves a value from the database by ID
|
|
2823
|
-
* @param id The identifier/key for the value
|
|
2824
|
-
* @returns The stored value or null if not found
|
|
2825
|
-
*/
|
|
2826
|
-
async getItem(id) {
|
|
2827
|
-
if (!this.db) {
|
|
2828
|
-
await this.init();
|
|
2829
|
-
}
|
|
2830
|
-
return new Promise((resolve, reject) => {
|
|
2831
|
-
const transaction = this.db.transaction(this.storeName, 'readonly');
|
|
2832
|
-
// Handle transation
|
|
2833
|
-
transaction.onerror = (event) => {
|
|
2834
|
-
reject(new Error(`Transaction error for getItem(${id}): ${event.target.error}`));
|
|
2835
|
-
};
|
|
2836
|
-
transaction.onabort = (event) => {
|
|
2837
|
-
reject(new Error(`Transaction aborted for getItem(${id}): ${event.target.error}`));
|
|
2838
|
-
};
|
|
2839
|
-
// Perform the request within the transaction
|
|
2840
|
-
const store = transaction.objectStore(this.storeName);
|
|
2841
|
-
const request = store.get(id);
|
|
2842
|
-
request.onsuccess = () => {
|
|
2843
|
-
resolve(request.result || null);
|
|
2844
|
-
};
|
|
2845
|
-
});
|
|
2846
|
-
}
|
|
2847
|
-
/**
|
|
2848
|
-
* Removes an item from the database
|
|
2849
|
-
* @param id The identifier of the item to remove
|
|
2850
|
-
*/
|
|
2851
|
-
async deleteItem(id) {
|
|
2852
|
-
if (!this.db) {
|
|
2853
|
-
await this.init();
|
|
2854
|
-
}
|
|
2855
|
-
return new Promise((resolve, reject) => {
|
|
2856
|
-
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2857
|
-
// Handle transation
|
|
2858
|
-
transaction.oncomplete = () => {
|
|
2859
|
-
resolve();
|
|
2860
|
-
};
|
|
2861
|
-
transaction.onerror = (event) => {
|
|
2862
|
-
reject(new Error(`Transaction error for deleteItem(${id}): ${event.target.error}`));
|
|
2863
|
-
};
|
|
2864
|
-
transaction.onabort = (event) => {
|
|
2865
|
-
reject(new Error(`Transaction aborted for deleteItem(${id}): ${event.target.error}`));
|
|
2866
|
-
};
|
|
2867
|
-
// Perform the request within the transaction
|
|
2868
|
-
const store = transaction.objectStore(this.storeName);
|
|
2869
|
-
store.delete(id);
|
|
2870
|
-
});
|
|
2871
|
-
}
|
|
2872
|
-
/**
|
|
2873
|
-
* Clears all items from the database
|
|
2874
|
-
*/
|
|
2875
|
-
async clear() {
|
|
2876
|
-
if (!this.db) {
|
|
2877
|
-
await this.init();
|
|
2878
|
-
}
|
|
2879
|
-
return new Promise((resolve, reject) => {
|
|
2880
|
-
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2881
|
-
// Handle transation
|
|
2882
|
-
transaction.oncomplete = () => {
|
|
2883
|
-
resolve();
|
|
2884
|
-
};
|
|
2885
|
-
transaction.onerror = (event) => {
|
|
2886
|
-
reject(new Error(`Transaction error for clear(): ${event.target.error}`));
|
|
2887
|
-
};
|
|
2888
|
-
transaction.onabort = (event) => {
|
|
2889
|
-
reject(new Error(`Transaction aborted for clear(): ${event.target.error}`));
|
|
2890
|
-
};
|
|
2891
|
-
// Perform the request within the transaction
|
|
2892
|
-
const store = transaction.objectStore(this.storeName);
|
|
2893
|
-
store.clear();
|
|
2894
|
-
});
|
|
2895
|
-
}
|
|
2896
|
-
/**
|
|
2897
|
-
* Closes the database connection
|
|
2898
|
-
*/
|
|
2899
|
-
close() {
|
|
2900
|
-
if (this.db) {
|
|
2901
|
-
this.db.close();
|
|
2902
|
-
this.db = null;
|
|
2903
|
-
}
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
var RefreshMessageTypes;
|
|
2908
|
-
(function (RefreshMessageTypes) {
|
|
2909
|
-
RefreshMessageTypes["SCHEDULE"] = "SCHEDULE";
|
|
2910
|
-
RefreshMessageTypes["REFRESH"] = "REFRESH";
|
|
2911
|
-
RefreshMessageTypes["STOP"] = "STOP";
|
|
2912
|
-
RefreshMessageTypes["DISCONNECT"] = "DISCONNECT";
|
|
2913
|
-
RefreshMessageTypes["TOKEN_DETAILS"] = "TOKEN_DETAILS";
|
|
2914
|
-
RefreshMessageTypes["ERROR_ON_REFRESH"] = "ERROR_ON_REFRESH";
|
|
2915
|
-
RefreshMessageTypes["EXPIRED"] = "EXPIRED";
|
|
2916
|
-
})(RefreshMessageTypes || (RefreshMessageTypes = {}));
|
|
2917
|
-
// A Set to store all connected ports (tabs)
|
|
2918
|
-
const ports = new Set();
|
|
2919
|
-
const broadcast = (message) => {
|
|
2920
|
-
for (const p of ports) {
|
|
2921
|
-
p.postMessage(message);
|
|
2922
|
-
}
|
|
2923
|
-
};
|
|
2924
|
-
let refresher;
|
|
2925
|
-
self.onconnect = (event) => {
|
|
2926
|
-
const port = event.ports[0];
|
|
2927
|
-
ports.add(port);
|
|
2928
|
-
// lazy init
|
|
2929
|
-
if (!refresher) {
|
|
2930
|
-
refresher = new Refresher(broadcast, new SessionIDB());
|
|
2931
|
-
}
|
|
2932
|
-
// handle messages
|
|
2933
|
-
port.onmessage = (event) => {
|
|
2934
|
-
const { type, payload } = event.data;
|
|
2935
|
-
switch (type) {
|
|
2936
|
-
case RefreshMessageTypes.SCHEDULE:
|
|
2937
|
-
refresher.handleSchedule(payload);
|
|
2938
|
-
break;
|
|
2939
|
-
case RefreshMessageTypes.REFRESH:
|
|
2940
|
-
refresher.handleRefresh(port);
|
|
2941
|
-
break;
|
|
2942
|
-
case RefreshMessageTypes.STOP:
|
|
2943
|
-
refresher.handleStop();
|
|
2944
|
-
break;
|
|
2945
|
-
case RefreshMessageTypes.DISCONNECT:
|
|
2946
|
-
ports.delete(port);
|
|
2947
|
-
break;
|
|
2948
|
-
}
|
|
2949
|
-
};
|
|
2950
|
-
port.onmessageerror = () => ports.delete(port);
|
|
2951
|
-
port.start();
|
|
2952
|
-
};
|
|
2953
|
-
class Refresher {
|
|
2954
|
-
tokenDetails;
|
|
2955
|
-
exp;
|
|
2956
|
-
refreshTimeout;
|
|
2957
|
-
finalLogoutTimeout;
|
|
2958
|
-
timersAreRunning = false;
|
|
2959
|
-
broadcast;
|
|
2960
|
-
database;
|
|
2961
|
-
refreshPromise;
|
|
2962
|
-
constructor(broadcast, database) {
|
|
2963
|
-
this.broadcast = broadcast;
|
|
2964
|
-
this.database = database;
|
|
2965
|
-
}
|
|
2966
|
-
async handleSchedule(tokenDetails) {
|
|
2967
|
-
this.tokenDetails = tokenDetails;
|
|
2968
|
-
this.exp = decodeJwt(this.tokenDetails.access_token).exp;
|
|
2969
|
-
this.broadcast({
|
|
2970
|
-
type: RefreshMessageTypes.TOKEN_DETAILS,
|
|
2971
|
-
payload: { tokenDetails: this.tokenDetails }
|
|
2972
|
-
});
|
|
2973
|
-
console.log(`[RefreshWorker] Scheduling timers, expiry in ${this.tokenDetails.expires_in}s`);
|
|
2974
|
-
this.scheduleTimers(this.tokenDetails.expires_in);
|
|
2975
|
-
this.timersAreRunning = true;
|
|
2976
|
-
}
|
|
2977
|
-
async handleRefresh(requestingPort) {
|
|
2978
|
-
if (this.tokenDetails && this.exp && !this.isTokenExpired(this.exp)) {
|
|
2979
|
-
console.log(`[RefreshWorker] Providing current tokens`);
|
|
2980
|
-
requestingPort.postMessage({
|
|
2981
|
-
type: RefreshMessageTypes.TOKEN_DETAILS,
|
|
2982
|
-
payload: { tokenDetails: this.tokenDetails }
|
|
2983
|
-
});
|
|
2984
|
-
}
|
|
2985
|
-
else {
|
|
2986
|
-
console.log(`[RefreshWorker] Refreshing tokens`);
|
|
2987
|
-
this.performRefresh();
|
|
2988
|
-
}
|
|
2989
|
-
}
|
|
2990
|
-
handleStop() {
|
|
2991
|
-
if (!this.tokenDetails) {
|
|
2992
|
-
console.log('[RefreshWorker] Received STOP, being idle');
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
|
-
this.broadcast({ type: RefreshMessageTypes.EXPIRED });
|
|
2996
|
-
this.tokenDetails = undefined;
|
|
2997
|
-
this.exp = undefined;
|
|
2998
|
-
this.refreshPromise = undefined;
|
|
2999
|
-
console.log('[RefreshWorker] Received STOP, clearing timers');
|
|
3000
|
-
this.clearAllTimers();
|
|
3001
|
-
}
|
|
3002
|
-
async performRefresh() {
|
|
3003
|
-
if (this.refreshPromise) {
|
|
3004
|
-
console.log('[RefreshWorker] Refresh already in progress, waiting...');
|
|
3005
|
-
return this.refreshPromise;
|
|
3006
|
-
}
|
|
3007
|
-
this.refreshPromise = this.doRefresh();
|
|
3008
|
-
return this.refreshPromise;
|
|
3009
|
-
}
|
|
3010
|
-
async doRefresh() {
|
|
3011
|
-
try {
|
|
3012
|
-
this.tokenDetails = await renewTokens(this.database);
|
|
3013
|
-
this.exp = decodeJwt(this.tokenDetails.access_token).exp;
|
|
3014
|
-
this.broadcast({
|
|
3015
|
-
type: RefreshMessageTypes.TOKEN_DETAILS,
|
|
3016
|
-
payload: { tokenDetails: this.tokenDetails }
|
|
3017
|
-
});
|
|
3018
|
-
console.log(`[RefreshWorker] Token refreshed`);
|
|
3019
|
-
console.log(`[RefreshWorker] Scheduling timers, expiry in ${this.tokenDetails.expires_in}s`);
|
|
3020
|
-
this.scheduleTimers(this.tokenDetails.expires_in);
|
|
3021
|
-
}
|
|
3022
|
-
catch (error) {
|
|
3023
|
-
this.broadcast({
|
|
3024
|
-
type: RefreshMessageTypes.ERROR_ON_REFRESH,
|
|
3025
|
-
error: error.message
|
|
3026
|
-
});
|
|
3027
|
-
console.log(`[RefreshWorker]`, error.message);
|
|
3028
|
-
}
|
|
3029
|
-
finally {
|
|
3030
|
-
this.refreshPromise = undefined;
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
clearAllTimers() {
|
|
3034
|
-
if (this.refreshTimeout)
|
|
3035
|
-
clearTimeout(this.refreshTimeout);
|
|
3036
|
-
if (this.finalLogoutTimeout)
|
|
3037
|
-
clearTimeout(this.finalLogoutTimeout);
|
|
3038
|
-
this.timersAreRunning = false;
|
|
3039
|
-
}
|
|
3040
|
-
scheduleTimers(expiresIn) {
|
|
3041
|
-
this.clearAllTimers();
|
|
3042
|
-
this.timersAreRunning = true;
|
|
3043
|
-
const expiresInMs = expiresIn * 1000;
|
|
3044
|
-
const REFRESH_THRESHOLD_RATIO = 0.8;
|
|
3045
|
-
const MINIMUM_REFRESH_BUFFER_MS = 30 * 1000;
|
|
3046
|
-
const timeUntilRefresh = REFRESH_THRESHOLD_RATIO * expiresInMs;
|
|
3047
|
-
if (timeUntilRefresh > MINIMUM_REFRESH_BUFFER_MS) {
|
|
3048
|
-
this.refreshTimeout = setTimeout(() => this.performRefresh(), timeUntilRefresh);
|
|
3049
|
-
}
|
|
3050
|
-
const LOGOUT_WARNING_BUFFER_MS = 5 * 1000;
|
|
3051
|
-
const timeUntilLogout = expiresInMs - LOGOUT_WARNING_BUFFER_MS;
|
|
3052
|
-
this.finalLogoutTimeout = setTimeout(() => {
|
|
3053
|
-
this.tokenDetails = undefined;
|
|
3054
|
-
this.broadcast({ type: RefreshMessageTypes.EXPIRED });
|
|
3055
|
-
}, timeUntilLogout);
|
|
3056
|
-
}
|
|
3057
|
-
isTokenExpired(exp, bufferSeconds = 0) {
|
|
3058
|
-
if (typeof exp !== 'number' || isNaN(exp)) {
|
|
3059
|
-
return true;
|
|
3060
|
-
}
|
|
3061
|
-
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
|
3062
|
-
return exp < (currentTimeSeconds + bufferSeconds);
|
|
3063
|
-
}
|
|
3064
|
-
setTokenDetails(tokenDetails) {
|
|
3065
|
-
this.tokenDetails = tokenDetails;
|
|
3066
|
-
}
|
|
3067
|
-
// For testing
|
|
3068
|
-
getTimersAreRunning() { return this.timersAreRunning; }
|
|
3069
|
-
getTokenDetails() { return this.tokenDetails; }
|
|
3070
|
-
}
|
|
3071
|
-
|
|
3072
|
-
/**
|
|
3073
|
-
* This Session provides background token refreshing using a Web Worker.
|
|
3074
|
-
*/
|
|
3075
|
-
class WebWorkerSession extends SessionCore {
|
|
3076
|
-
worker;
|
|
3077
|
-
onSessionExpirationWarning;
|
|
3078
|
-
onSessionExpiration;
|
|
3079
|
-
constructor(clientDetails, sessionOptions) {
|
|
3080
|
-
const database = new SessionIDB();
|
|
3081
|
-
const options = { ...sessionOptions, database };
|
|
3082
|
-
super(clientDetails, options);
|
|
3083
|
-
this.onSessionExpirationWarning = sessionOptions?.onSessionExpirationWarning;
|
|
3084
|
-
this.onSessionExpiration = sessionOptions?.onSessionExpiration;
|
|
3085
|
-
// Allow consumer to provide worker URL, or use default
|
|
3086
|
-
const workerUrl = sessionOptions?.workerUrl ?? getWorkerUrl();
|
|
3087
|
-
this.worker = new SharedWorker(workerUrl, { type: 'module' });
|
|
3088
|
-
this.worker.port.onmessage = (event) => {
|
|
3089
|
-
this.handleWorkerMessage(event.data).catch(console.error);
|
|
3090
|
-
};
|
|
3091
|
-
window.addEventListener('beforeunload', () => {
|
|
3092
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.DISCONNECT });
|
|
3093
|
-
});
|
|
3094
|
-
}
|
|
3095
|
-
async handleWorkerMessage(data) {
|
|
3096
|
-
const { type, payload, error } = data;
|
|
3097
|
-
switch (type) {
|
|
3098
|
-
case RefreshMessageTypes.TOKEN_DETAILS:
|
|
3099
|
-
const wasActive = this.isActive;
|
|
3100
|
-
await this.setTokenDetails(payload.tokenDetails);
|
|
3101
|
-
if (wasActive !== this.isActive)
|
|
3102
|
-
this.onSessionStateChange?.();
|
|
3103
|
-
if (this.refreshPromise && this.resolveRefresh) {
|
|
3104
|
-
this.resolveRefresh();
|
|
3105
|
-
this.clearRefreshPromise();
|
|
3106
|
-
}
|
|
3107
|
-
break;
|
|
3108
|
-
case RefreshMessageTypes.ERROR_ON_REFRESH:
|
|
3109
|
-
if (this.isActive)
|
|
3110
|
-
this.onSessionExpirationWarning?.();
|
|
3111
|
-
if (this.refreshPromise && this.rejectRefresh) {
|
|
3112
|
-
if (this.isActive) {
|
|
3113
|
-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
|
|
3114
|
-
}
|
|
3115
|
-
else {
|
|
3116
|
-
this.rejectRefresh(new Error("No session to restore"));
|
|
3117
|
-
}
|
|
3118
|
-
this.clearRefreshPromise();
|
|
3119
|
-
}
|
|
3120
|
-
break;
|
|
3121
|
-
case RefreshMessageTypes.EXPIRED:
|
|
3122
|
-
if (this.isActive) {
|
|
3123
|
-
this.onSessionExpiration?.();
|
|
3124
|
-
await this.logout();
|
|
3125
|
-
}
|
|
3126
|
-
if (this.refreshPromise && this.rejectRefresh) {
|
|
3127
|
-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
|
|
3128
|
-
this.clearRefreshPromise();
|
|
3129
|
-
}
|
|
3130
|
-
break;
|
|
3131
|
-
}
|
|
3132
|
-
}
|
|
3133
|
-
;
|
|
3134
|
-
async handleRedirectFromLogin() {
|
|
3135
|
-
await super.handleRedirectFromLogin();
|
|
3136
|
-
if (this.isActive) { // If login was successful, tell the worker to schedule refreshing
|
|
3137
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.SCHEDULE, payload: this.getTokenDetails() });
|
|
3138
|
-
}
|
|
3139
|
-
}
|
|
3140
|
-
async restore() {
|
|
3141
|
-
if (this.refreshPromise) {
|
|
3142
|
-
return this.refreshPromise;
|
|
3143
|
-
}
|
|
3144
|
-
this.refreshPromise = new Promise((resolve, reject) => {
|
|
3145
|
-
this.resolveRefresh = resolve;
|
|
3146
|
-
this.rejectRefresh = reject;
|
|
3147
|
-
});
|
|
3148
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.REFRESH });
|
|
3149
|
-
return this.refreshPromise;
|
|
3150
|
-
}
|
|
3151
|
-
async logout() {
|
|
3152
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.STOP });
|
|
3153
|
-
await super.logout();
|
|
3154
|
-
}
|
|
3155
2826
|
}
|
|
3156
2827
|
|
|
3157
2828
|
class BrowserSession {
|
|
@@ -3164,7 +2835,7 @@ class BrowserSession {
|
|
|
3164
2835
|
isLoggedIn: false,
|
|
3165
2836
|
webId: undefined,
|
|
3166
2837
|
});
|
|
3167
|
-
this.session = new
|
|
2838
|
+
this.session = new Session();
|
|
3168
2839
|
this._authenticatedFetch = this.session.authFetch.bind(this.session);
|
|
3169
2840
|
}
|
|
3170
2841
|
async handleIncomingRedirect(restorePreviousSession = false) {
|