@pod-os/elements 0.31.1-rc.fd664af.0 → 0.32.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
|
@@ -2612,6 +2612,160 @@ const requestDynamicClientRegistration = async (registration_endpoint, client_de
|
|
|
2612
2612
|
});
|
|
2613
2613
|
};
|
|
2614
2614
|
|
|
2615
|
+
/**
|
|
2616
|
+
* A simple IndexedDB wrapper
|
|
2617
|
+
*/
|
|
2618
|
+
class SessionDatabase {
|
|
2619
|
+
dbName;
|
|
2620
|
+
storeName;
|
|
2621
|
+
dbVersion;
|
|
2622
|
+
db = null;
|
|
2623
|
+
/**
|
|
2624
|
+
* Creates a new instance
|
|
2625
|
+
* @param dbName The name of the IndexedDB database
|
|
2626
|
+
* @param storeName The name of the object store
|
|
2627
|
+
* @param dbVersion The database version
|
|
2628
|
+
*/
|
|
2629
|
+
constructor(dbName = 'soidc', storeName = 'session', dbVersion = 1) {
|
|
2630
|
+
this.dbName = dbName;
|
|
2631
|
+
this.storeName = storeName;
|
|
2632
|
+
this.dbVersion = dbVersion;
|
|
2633
|
+
}
|
|
2634
|
+
/**
|
|
2635
|
+
* Initializes the IndexedDB database
|
|
2636
|
+
* @returns Promise that resolves when the database is ready
|
|
2637
|
+
*/
|
|
2638
|
+
async init() {
|
|
2639
|
+
return new Promise((resolve, reject) => {
|
|
2640
|
+
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
2641
|
+
request.onerror = (event) => {
|
|
2642
|
+
reject(new Error(`Database error: ${event.target.error}`));
|
|
2643
|
+
};
|
|
2644
|
+
request.onsuccess = (event) => {
|
|
2645
|
+
this.db = event.target.result;
|
|
2646
|
+
resolve(this);
|
|
2647
|
+
};
|
|
2648
|
+
request.onupgradeneeded = (event) => {
|
|
2649
|
+
const db = event.target.result;
|
|
2650
|
+
// Check if the object store already exists, if not create it
|
|
2651
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
2652
|
+
db.createObjectStore(this.storeName);
|
|
2653
|
+
}
|
|
2654
|
+
};
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
/**
|
|
2658
|
+
* Stores any value in the database with the given ID as key
|
|
2659
|
+
* @param id The identifier/key for the value
|
|
2660
|
+
* @param value The value to store
|
|
2661
|
+
*/
|
|
2662
|
+
async setItem(id, value) {
|
|
2663
|
+
if (!this.db) {
|
|
2664
|
+
await this.init();
|
|
2665
|
+
}
|
|
2666
|
+
return new Promise((resolve, reject) => {
|
|
2667
|
+
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2668
|
+
// Handle transation
|
|
2669
|
+
transaction.oncomplete = () => {
|
|
2670
|
+
resolve();
|
|
2671
|
+
};
|
|
2672
|
+
transaction.onerror = (event) => {
|
|
2673
|
+
reject(new Error(`Transaction error for setItem(${id},...): ${event.target.error}`));
|
|
2674
|
+
};
|
|
2675
|
+
transaction.onabort = (event) => {
|
|
2676
|
+
reject(new Error(`Transaction aborted for setItem(${id},...): ${event.target.error}`));
|
|
2677
|
+
};
|
|
2678
|
+
// Perform the request within the transaction
|
|
2679
|
+
const store = transaction.objectStore(this.storeName);
|
|
2680
|
+
store.put(value, id);
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
/**
|
|
2684
|
+
* Retrieves a value from the database by ID
|
|
2685
|
+
* @param id The identifier/key for the value
|
|
2686
|
+
* @returns The stored value or null if not found
|
|
2687
|
+
*/
|
|
2688
|
+
async getItem(id) {
|
|
2689
|
+
if (!this.db) {
|
|
2690
|
+
await this.init();
|
|
2691
|
+
}
|
|
2692
|
+
return new Promise((resolve, reject) => {
|
|
2693
|
+
const transaction = this.db.transaction(this.storeName, 'readonly');
|
|
2694
|
+
// Handle transation
|
|
2695
|
+
transaction.onerror = (event) => {
|
|
2696
|
+
reject(new Error(`Transaction error for getItem(${id}): ${event.target.error}`));
|
|
2697
|
+
};
|
|
2698
|
+
transaction.onabort = (event) => {
|
|
2699
|
+
reject(new Error(`Transaction aborted for getItem(${id}): ${event.target.error}`));
|
|
2700
|
+
};
|
|
2701
|
+
// Perform the request within the transaction
|
|
2702
|
+
const store = transaction.objectStore(this.storeName);
|
|
2703
|
+
const request = store.get(id);
|
|
2704
|
+
request.onsuccess = () => {
|
|
2705
|
+
resolve(request.result || null);
|
|
2706
|
+
};
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Removes an item from the database
|
|
2711
|
+
* @param id The identifier of the item to remove
|
|
2712
|
+
*/
|
|
2713
|
+
async deleteItem(id) {
|
|
2714
|
+
if (!this.db) {
|
|
2715
|
+
await this.init();
|
|
2716
|
+
}
|
|
2717
|
+
return new Promise((resolve, reject) => {
|
|
2718
|
+
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2719
|
+
// Handle transation
|
|
2720
|
+
transaction.oncomplete = () => {
|
|
2721
|
+
resolve();
|
|
2722
|
+
};
|
|
2723
|
+
transaction.onerror = (event) => {
|
|
2724
|
+
reject(new Error(`Transaction error for deleteItem(${id}): ${event.target.error}`));
|
|
2725
|
+
};
|
|
2726
|
+
transaction.onabort = (event) => {
|
|
2727
|
+
reject(new Error(`Transaction aborted for deleteItem(${id}): ${event.target.error}`));
|
|
2728
|
+
};
|
|
2729
|
+
// Perform the request within the transaction
|
|
2730
|
+
const store = transaction.objectStore(this.storeName);
|
|
2731
|
+
store.delete(id);
|
|
2732
|
+
});
|
|
2733
|
+
}
|
|
2734
|
+
/**
|
|
2735
|
+
* Clears all items from the database
|
|
2736
|
+
*/
|
|
2737
|
+
async clear() {
|
|
2738
|
+
if (!this.db) {
|
|
2739
|
+
await this.init();
|
|
2740
|
+
}
|
|
2741
|
+
return new Promise((resolve, reject) => {
|
|
2742
|
+
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
2743
|
+
// Handle transation
|
|
2744
|
+
transaction.oncomplete = () => {
|
|
2745
|
+
resolve();
|
|
2746
|
+
};
|
|
2747
|
+
transaction.onerror = (event) => {
|
|
2748
|
+
reject(new Error(`Transaction error for clear(): ${event.target.error}`));
|
|
2749
|
+
};
|
|
2750
|
+
transaction.onabort = (event) => {
|
|
2751
|
+
reject(new Error(`Transaction aborted for clear(): ${event.target.error}`));
|
|
2752
|
+
};
|
|
2753
|
+
// Perform the request within the transaction
|
|
2754
|
+
const store = transaction.objectStore(this.storeName);
|
|
2755
|
+
store.clear();
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
/**
|
|
2759
|
+
* Closes the database connection
|
|
2760
|
+
*/
|
|
2761
|
+
close() {
|
|
2762
|
+
if (this.db) {
|
|
2763
|
+
this.db.close();
|
|
2764
|
+
this.db = null;
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2615
2769
|
/**
|
|
2616
2770
|
* Login with the idp, using a provided `client_id` or dynamic client registration if none provided.
|
|
2617
2771
|
*
|
|
@@ -2635,7 +2789,7 @@ const redirectForLogin = async (idp, redirect_uri, client_details) => {
|
|
|
2635
2789
|
const issuer = openid_configuration["issuer"];
|
|
2636
2790
|
const trim_trailing_slash = (url) => (url.endsWith('/') ? url.slice(0, -1) : url);
|
|
2637
2791
|
if (trim_trailing_slash(idp) !== trim_trailing_slash(issuer)) { // expected idp matches received issuer mod trailing slash?
|
|
2638
|
-
throw new Error("RFC 9207 - iss
|
|
2792
|
+
throw new Error("RFC 9207 - iss != idp - " + issuer + " != " + idp);
|
|
2639
2793
|
}
|
|
2640
2794
|
sessionStorage.setItem("idp", issuer);
|
|
2641
2795
|
// remember token endpoint
|
|
@@ -2656,12 +2810,7 @@ const redirectForLogin = async (idp, redirect_uri, client_details) => {
|
|
|
2656
2810
|
return response.json();
|
|
2657
2811
|
});
|
|
2658
2812
|
client_id = client_registration["client_id"];
|
|
2659
|
-
|
|
2660
|
-
// remember client_id if not URL
|
|
2661
|
-
try {
|
|
2662
|
-
new URL(client_id);
|
|
2663
|
-
}
|
|
2664
|
-
catch {
|
|
2813
|
+
// remember client_id
|
|
2665
2814
|
sessionStorage.setItem("client_id", client_id);
|
|
2666
2815
|
}
|
|
2667
2816
|
// RFC 7636 PKCE, remember code verifer
|
|
@@ -2702,7 +2851,7 @@ const getPKCEcode = async () => {
|
|
|
2702
2851
|
* URL contains authrization code, issuer (idp) and state (csrf token),
|
|
2703
2852
|
* get an access token for the authrization code.
|
|
2704
2853
|
*/
|
|
2705
|
-
const onIncomingRedirect = async (client_details
|
|
2854
|
+
const onIncomingRedirect = async (client_details) => {
|
|
2706
2855
|
const url = new URL(window.location.href);
|
|
2707
2856
|
// authorization code
|
|
2708
2857
|
const authorization_code = url.searchParams.get("code");
|
|
@@ -2712,12 +2861,12 @@ const onIncomingRedirect = async (client_details, database) => {
|
|
|
2712
2861
|
}
|
|
2713
2862
|
// RFC 9207 issuer check
|
|
2714
2863
|
const idp = sessionStorage.getItem("idp");
|
|
2715
|
-
if (idp === null || url.searchParams.get("iss")
|
|
2716
|
-
throw new Error("RFC 9207 - iss
|
|
2864
|
+
if (idp === null || url.searchParams.get("iss") != idp) {
|
|
2865
|
+
throw new Error("RFC 9207 - iss != idp - " + url.searchParams.get("iss") + " != " + idp);
|
|
2717
2866
|
}
|
|
2718
2867
|
// RFC 6749 OAuth 2.0
|
|
2719
|
-
if (url.searchParams.get("state")
|
|
2720
|
-
throw new Error("RFC 6749 - state
|
|
2868
|
+
if (url.searchParams.get("state") != sessionStorage.getItem("csrf_token")) {
|
|
2869
|
+
throw new Error("RFC 6749 - state != csrf_token - " + url.searchParams.get("state") + " != " + sessionStorage.getItem("csrf_token"));
|
|
2721
2870
|
}
|
|
2722
2871
|
// remove redirect query parameters from URL
|
|
2723
2872
|
url.searchParams.delete("iss");
|
|
@@ -2761,32 +2910,30 @@ const onIncomingRedirect = async (client_details, database) => {
|
|
|
2761
2910
|
});
|
|
2762
2911
|
// check dpop thumbprint
|
|
2763
2912
|
const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
|
|
2764
|
-
if (payload["cnf"]["jkt"]
|
|
2765
|
-
throw new Error("Access Token validation failed on `jkt`: jkt
|
|
2913
|
+
if (payload["cnf"]["jkt"] != dpopThumbprint) {
|
|
2914
|
+
throw new Error("Access Token validation failed on `jkt`: jkt != DPoP thumbprint - " + payload["cnf"]["jkt"] + " != " + dpopThumbprint);
|
|
2766
2915
|
}
|
|
2767
2916
|
// check client_id
|
|
2768
|
-
if (payload["client_id"]
|
|
2769
|
-
throw new Error("Access Token validation failed on `client_id`: JWT payload
|
|
2917
|
+
if (payload["client_id"] != client_id) {
|
|
2918
|
+
throw new Error("Access Token validation failed on `client_id`: JWT payload != client_id - " + payload["client_id"] + " != " + client_id);
|
|
2770
2919
|
}
|
|
2771
2920
|
// summarise session info
|
|
2772
2921
|
const token_details = { ...token_response, dpop_key_pair: key_pair };
|
|
2773
2922
|
const idp_details = { idp, jwks_uri, token_endpoint };
|
|
2774
2923
|
if (!client_details)
|
|
2775
|
-
client_details = { redirect_uris: [
|
|
2924
|
+
client_details = { redirect_uris: [window.location.href] };
|
|
2776
2925
|
client_details.client_id = client_id;
|
|
2777
|
-
//
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
database.close();
|
|
2789
|
-
}
|
|
2926
|
+
// to remember for session restore
|
|
2927
|
+
const sessionDatabase = await new SessionDatabase().init();
|
|
2928
|
+
await Promise.all([
|
|
2929
|
+
sessionDatabase.setItem("idp", idp),
|
|
2930
|
+
sessionDatabase.setItem("jwks_uri", jwks_uri),
|
|
2931
|
+
sessionDatabase.setItem("token_endpoint", token_endpoint),
|
|
2932
|
+
sessionDatabase.setItem("client_id", client_id),
|
|
2933
|
+
sessionDatabase.setItem("dpop_keypair", key_pair),
|
|
2934
|
+
sessionDatabase.setItem("refresh_token", token_response["refresh_token"])
|
|
2935
|
+
]);
|
|
2936
|
+
sessionDatabase.close();
|
|
2790
2937
|
// clean session storage
|
|
2791
2938
|
sessionStorage.removeItem("csrf_token");
|
|
2792
2939
|
sessionStorage.removeItem("pkce_code_verifier");
|
|
@@ -2844,60 +2991,56 @@ const requestAccessToken = async (authorization_code, pkce_code_verifier, redire
|
|
|
2844
2991
|
});
|
|
2845
2992
|
};
|
|
2846
2993
|
|
|
2847
|
-
const renewTokens = async (
|
|
2994
|
+
const renewTokens = async () => {
|
|
2848
2995
|
// remember session details
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
2863
|
-
}
|
|
2864
|
-
return response.json();
|
|
2865
|
-
});
|
|
2866
|
-
// verify access_token // ! Solid-OIDC specification says it should be a dpop-bound `id token` but implementations provide a dpop-bound `access token`
|
|
2867
|
-
const accessToken = token_response["access_token"];
|
|
2868
|
-
const idp = await sessionDatabase.getItem("idp");
|
|
2869
|
-
if (idp === null) {
|
|
2870
|
-
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: idp");
|
|
2871
|
-
}
|
|
2872
|
-
const jwks_uri = await sessionDatabase.getItem("jwks_uri");
|
|
2873
|
-
if (jwks_uri === null) {
|
|
2874
|
-
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: jwks_uri");
|
|
2875
|
-
}
|
|
2876
|
-
const jwks = createRemoteJWKSet(new URL(jwks_uri));
|
|
2877
|
-
const { payload } = await jwtVerify(accessToken, jwks, {
|
|
2878
|
-
issuer: idp, // RFC 9207
|
|
2879
|
-
audience: "solid", // RFC 7519 // ! "solid" as per implementations ...
|
|
2880
|
-
// exp, nbf, iat - handled automatically
|
|
2881
|
-
});
|
|
2882
|
-
// check dpop thumbprint
|
|
2883
|
-
const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
|
|
2884
|
-
if (payload["cnf"]["jkt"] !== dpopThumbprint) {
|
|
2885
|
-
throw new Error("Access Token validation failed on `jkt`: jkt !== DPoP thumbprint - " + payload["cnf"]["jkt"] + " !== " + dpopThumbprint);
|
|
2886
|
-
}
|
|
2887
|
-
// check client_id
|
|
2888
|
-
if (payload["client_id"] !== client_id) {
|
|
2889
|
-
throw new Error("Access Token validation failed on `client_id`: JWT payload !== client_id - " + payload["client_id"] + " !== " + client_id);
|
|
2996
|
+
const sessionDatabase = await new SessionDatabase().init();
|
|
2997
|
+
const client_id = await sessionDatabase.getItem("client_id");
|
|
2998
|
+
const token_endpoint = await sessionDatabase.getItem("token_endpoint");
|
|
2999
|
+
const key_pair = await sessionDatabase.getItem("dpop_keypair");
|
|
3000
|
+
const refresh_token = await sessionDatabase.getItem("refresh_token");
|
|
3001
|
+
if (client_id === null || token_endpoint === null || key_pair === null || refresh_token === null) {
|
|
3002
|
+
// we can not restore the old session
|
|
3003
|
+
throw new Error("Could not refresh tokens: details missing from database.");
|
|
3004
|
+
}
|
|
3005
|
+
const token_response = await requestFreshTokens(refresh_token, client_id, token_endpoint, key_pair)
|
|
3006
|
+
.then((response) => {
|
|
3007
|
+
if (!response.ok) {
|
|
3008
|
+
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
2890
3009
|
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
3010
|
+
return response.json();
|
|
3011
|
+
});
|
|
3012
|
+
// verify access_token // ! Solid-OIDC specification says it should be a dpop-bound `id token` but implementations provide a dpop-bound `access token`
|
|
3013
|
+
const accessToken = token_response["access_token"];
|
|
3014
|
+
const idp = await sessionDatabase.getItem("idp");
|
|
3015
|
+
if (idp === null) {
|
|
3016
|
+
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: idp");
|
|
2897
3017
|
}
|
|
2898
|
-
|
|
2899
|
-
|
|
3018
|
+
const jwks_uri = await sessionDatabase.getItem("jwks_uri");
|
|
3019
|
+
if (jwks_uri === null) {
|
|
3020
|
+
throw new Error("Access Token validation preparation - Could not find in sessionDatabase: jwks_uri");
|
|
2900
3021
|
}
|
|
3022
|
+
const jwks = createRemoteJWKSet(new URL(jwks_uri));
|
|
3023
|
+
const { payload } = await jwtVerify(accessToken, jwks, {
|
|
3024
|
+
issuer: idp, // RFC 9207
|
|
3025
|
+
audience: "solid", // RFC 7519 // ! "solid" as per implementations ...
|
|
3026
|
+
// exp, nbf, iat - handled automatically
|
|
3027
|
+
});
|
|
3028
|
+
// check dpop thumbprint
|
|
3029
|
+
const dpopThumbprint = await calculateJwkThumbprint(await exportJWK(key_pair.publicKey));
|
|
3030
|
+
if (payload["cnf"]["jkt"] != dpopThumbprint) {
|
|
3031
|
+
throw new Error("Access Token validation failed on `jkt`: jkt != DPoP thumbprint - " + payload["cnf"]["jkt"] + " != " + dpopThumbprint);
|
|
3032
|
+
}
|
|
3033
|
+
// check client_id
|
|
3034
|
+
if (payload["client_id"] != client_id) {
|
|
3035
|
+
throw new Error("Access Token validation failed on `client_id`: JWT payload != client_id - " + payload["client_id"] + " != " + client_id);
|
|
3036
|
+
}
|
|
3037
|
+
// set new refresh token for token rotation
|
|
3038
|
+
await sessionDatabase.setItem("refresh_token", token_response["refresh_token"]);
|
|
3039
|
+
sessionDatabase.close();
|
|
3040
|
+
return {
|
|
3041
|
+
...token_response,
|
|
3042
|
+
dpop_key_pair: key_pair,
|
|
3043
|
+
};
|
|
2901
3044
|
};
|
|
2902
3045
|
/**
|
|
2903
3046
|
* Request an dpop-bound access token from a token endpoint using a refresh token
|
|
@@ -2919,7 +3062,7 @@ const requestFreshTokens = async (refresh_token, client_id, token_endpoint, key_
|
|
|
2919
3062
|
htm: "POST",
|
|
2920
3063
|
})
|
|
2921
3064
|
.setIssuedAt()
|
|
2922
|
-
.setJti(
|
|
3065
|
+
.setJti(window.crypto.randomUUID())
|
|
2923
3066
|
.setProtectedHeader({
|
|
2924
3067
|
alg: "ES256",
|
|
2925
3068
|
typ: "dpop+jwt",
|
|
@@ -2940,118 +3083,113 @@ const requestFreshTokens = async (refresh_token, client_id, token_endpoint, key_
|
|
|
2940
3083
|
});
|
|
2941
3084
|
};
|
|
2942
3085
|
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
// Basic implementation
|
|
2946
|
-
//
|
|
2947
|
-
//
|
|
2948
|
-
/**
|
|
2949
|
-
* The SessionCore class manages session state and core logic but does not handle the refresh lifecycle.
|
|
2950
|
-
* It receives {@link SessionOptions} with a database to be able to restore a session.
|
|
2951
|
-
* That database can be re-used by (your!) surrounding implementation to handle the refresh lifecycle.
|
|
2952
|
-
* 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.
|
|
2953
|
-
*
|
|
2954
|
-
* If you are building a web app, use the Session implementation provided in the default `/web` version of this library.
|
|
2955
|
-
*/
|
|
2956
|
-
class SessionCore {
|
|
3086
|
+
class Session {
|
|
3087
|
+
sessionInformation;
|
|
2957
3088
|
isActive_ = false;
|
|
2958
|
-
exp_;
|
|
2959
3089
|
webId_ = undefined;
|
|
2960
3090
|
currentAth_ = undefined;
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3091
|
+
tokenRefreshTimeout;
|
|
3092
|
+
sessionDeactivateTimeout;
|
|
3093
|
+
onSessionExpirationWarning;
|
|
3094
|
+
/**
|
|
3095
|
+
* Create a new session.
|
|
3096
|
+
*/
|
|
2967
3097
|
constructor(clientDetails, sessionOptions) {
|
|
2968
|
-
this.
|
|
2969
|
-
this.
|
|
2970
|
-
this.onSessionStateChange = sessionOptions?.onSessionStateChange;
|
|
3098
|
+
this.sessionInformation = { clientDetails };
|
|
3099
|
+
this.onSessionExpirationWarning = sessionOptions?.onSessionExpirationWarning;
|
|
2971
3100
|
}
|
|
3101
|
+
/**
|
|
3102
|
+
* Redirect the user for login to their IDP.
|
|
3103
|
+
*
|
|
3104
|
+
* @throws Error if the session has not been initialized.
|
|
3105
|
+
*/
|
|
2972
3106
|
async login(idp, redirect_uri) {
|
|
2973
|
-
await redirectForLogin(idp, redirect_uri, this.
|
|
3107
|
+
await redirectForLogin(idp, redirect_uri, this.sessionInformation.clientDetails);
|
|
3108
|
+
}
|
|
3109
|
+
/**
|
|
3110
|
+
* Clears all session-related information, including IDP details and tokens.
|
|
3111
|
+
* This logs the user out.
|
|
3112
|
+
* Client details are preserved.
|
|
3113
|
+
*/
|
|
3114
|
+
async logout() {
|
|
3115
|
+
// clear timeouts
|
|
3116
|
+
if (this.sessionDeactivateTimeout)
|
|
3117
|
+
clearTimeout(this.sessionDeactivateTimeout);
|
|
3118
|
+
this.sessionDeactivateTimeout = undefined;
|
|
3119
|
+
if (this.tokenRefreshTimeout)
|
|
3120
|
+
clearTimeout(this.tokenRefreshTimeout);
|
|
3121
|
+
this.tokenRefreshTimeout = undefined;
|
|
3122
|
+
// clean session data
|
|
3123
|
+
this.sessionInformation.idpDetails = undefined;
|
|
3124
|
+
this.sessionInformation.tokenDetails = undefined;
|
|
3125
|
+
this.isActive_ = false;
|
|
3126
|
+
this.webId_ = undefined;
|
|
3127
|
+
// only preserve client_id if URI
|
|
3128
|
+
if (this.sessionInformation.clientDetails?.client_id)
|
|
3129
|
+
try {
|
|
3130
|
+
new URL(this.sessionInformation.clientDetails.client_id);
|
|
3131
|
+
}
|
|
3132
|
+
catch (_) {
|
|
3133
|
+
this.sessionInformation.clientDetails.client_id = undefined;
|
|
3134
|
+
}
|
|
3135
|
+
// clean session database
|
|
3136
|
+
const sessionDatabase = await new SessionDatabase().init();
|
|
3137
|
+
await sessionDatabase.clear();
|
|
3138
|
+
sessionDatabase.close();
|
|
2974
3139
|
}
|
|
2975
3140
|
/**
|
|
2976
3141
|
* Handles the redirect from the identity provider after a login attempt.
|
|
2977
3142
|
* It attempts to retrieve tokens using the authorization code.
|
|
2978
|
-
* Upon success, it tries to persist information to refresh tokens in the session database.
|
|
2979
|
-
* If no database was provided, no information is persisted.
|
|
2980
3143
|
*/
|
|
2981
3144
|
async handleRedirectFromLogin() {
|
|
2982
3145
|
// Redirect after Authorization Code Grant // memory via sessionStorage
|
|
2983
|
-
const newSessionInfo = await onIncomingRedirect(this.
|
|
3146
|
+
const newSessionInfo = await onIncomingRedirect(this.sessionInformation.clientDetails);
|
|
2984
3147
|
// no session - we remain unauthenticated
|
|
2985
|
-
if (!newSessionInfo.tokenDetails)
|
|
3148
|
+
if (!newSessionInfo.tokenDetails) {
|
|
2986
3149
|
return;
|
|
3150
|
+
}
|
|
2987
3151
|
// we got a session
|
|
2988
|
-
this.
|
|
2989
|
-
this.
|
|
2990
|
-
await this.setTokenDetails(newSessionInfo.tokenDetails);
|
|
2991
|
-
// callback state change
|
|
2992
|
-
this.onSessionStateChange?.(); // we logged in
|
|
3152
|
+
this.sessionInformation = newSessionInfo;
|
|
3153
|
+
await this.setSessionDetails();
|
|
2993
3154
|
}
|
|
2994
3155
|
/**
|
|
2995
3156
|
* Handles session restoration using the refresh token grant.
|
|
2996
3157
|
* Silently fails if session could not be restored (maybe there was no session in the first place).
|
|
2997
3158
|
*/
|
|
2998
3159
|
async restore() {
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
// Restore session using Refresh Token Grant
|
|
3010
|
-
const wasActive = this.isActive;
|
|
3011
|
-
renewTokens(this.database)
|
|
3012
|
-
.then(tokenDetails => this.setTokenDetails(tokenDetails))
|
|
3013
|
-
.then(() => this.resolveRefresh())
|
|
3014
|
-
.catch(error => {
|
|
3015
|
-
if (this.isActive) {
|
|
3016
|
-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
|
|
3017
|
-
// do not change state (yet), let the app decide if they want to logout or if they just want to retry.
|
|
3018
|
-
}
|
|
3019
|
-
else {
|
|
3020
|
-
this.rejectRefresh(new Error("No session to restore."));
|
|
3021
|
-
}
|
|
3022
|
-
}).finally(() => {
|
|
3023
|
-
this.clearRefreshPromise();
|
|
3024
|
-
if (wasActive !== this.isActive)
|
|
3025
|
-
this.onSessionStateChange?.();
|
|
3026
|
-
});
|
|
3027
|
-
return this.refreshPromise;
|
|
3160
|
+
// Restore session using Refresh Token Grant // memory via IndexedDB
|
|
3161
|
+
await renewTokens()
|
|
3162
|
+
.then(tokenDetails => {
|
|
3163
|
+
// got new tokens
|
|
3164
|
+
this.sessionInformation.tokenDetails = tokenDetails;
|
|
3165
|
+
// set session information
|
|
3166
|
+
return this.setSessionDetails();
|
|
3167
|
+
})
|
|
3168
|
+
// anything missing or wrong => abort, could not restore session.
|
|
3169
|
+
.catch(_ => { }); // fail silently
|
|
3028
3170
|
}
|
|
3029
3171
|
/**
|
|
3030
|
-
*
|
|
3031
|
-
*
|
|
3032
|
-
*
|
|
3172
|
+
* Creates a signed DPoP (Demonstration of Proof-of-Possession) token.
|
|
3173
|
+
*
|
|
3174
|
+
* @param payload The payload to include in the DPoP token. By default, it includes `htu` (HTTP target URI) and `htm` (HTTP method).
|
|
3175
|
+
* @returns A promise that resolves to the signed DPoP token string.
|
|
3176
|
+
* @throws Error if the session has not been initialized - if no token details are available.
|
|
3033
3177
|
*/
|
|
3034
|
-
async
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
this.exp_ = undefined;
|
|
3038
|
-
this.webId_ = undefined;
|
|
3039
|
-
this.currentAth_ = undefined;
|
|
3040
|
-
this.information.idpDetails = undefined;
|
|
3041
|
-
this.information.tokenDetails = undefined;
|
|
3042
|
-
// client details are preserved
|
|
3043
|
-
if (this.refreshPromise && this.rejectRefresh) {
|
|
3044
|
-
this.rejectRefresh(new Error('Logout during token refresh.'));
|
|
3045
|
-
this.clearRefreshPromise();
|
|
3046
|
-
}
|
|
3047
|
-
// clean session database
|
|
3048
|
-
if (this.database) {
|
|
3049
|
-
await this.database.init();
|
|
3050
|
-
await this.database.clear();
|
|
3051
|
-
this.database.close();
|
|
3178
|
+
async createSignedDPoPToken(payload) {
|
|
3179
|
+
if (!this.sessionInformation.tokenDetails || !this.currentAth_) {
|
|
3180
|
+
throw new Error("Session not established.");
|
|
3052
3181
|
}
|
|
3053
|
-
|
|
3054
|
-
this.
|
|
3182
|
+
payload.ath = this.currentAth_;
|
|
3183
|
+
const jwk_public_key = await exportJWK(this.sessionInformation.tokenDetails.dpop_key_pair.publicKey);
|
|
3184
|
+
return new SignJWT(payload)
|
|
3185
|
+
.setIssuedAt()
|
|
3186
|
+
.setJti(window.crypto.randomUUID())
|
|
3187
|
+
.setProtectedHeader({
|
|
3188
|
+
alg: "ES256",
|
|
3189
|
+
typ: "dpop+jwt",
|
|
3190
|
+
jwk: jwk_public_key,
|
|
3191
|
+
})
|
|
3192
|
+
.sign(this.sessionInformation.tokenDetails.dpop_key_pair.privateKey);
|
|
3055
3193
|
}
|
|
3056
3194
|
/**
|
|
3057
3195
|
* Makes an HTTP fetch request.
|
|
@@ -3063,18 +3201,10 @@ class SessionCore {
|
|
|
3063
3201
|
* @returns A promise that resolves to the fetch Response.
|
|
3064
3202
|
*/
|
|
3065
3203
|
async authFetch(input, init, dpopPayload) {
|
|
3066
|
-
// if there is not session established, just delegate to the default fetch
|
|
3067
|
-
if (!this.isActive) {
|
|
3068
|
-
return fetch(input, init);
|
|
3069
|
-
}
|
|
3070
|
-
// TODO
|
|
3071
|
-
// TODO do HEAD request to check if authentication is actually required, only then include tokens
|
|
3072
|
-
// TODO
|
|
3073
3204
|
// prepare authenticated call using a DPoP token (either provided payload, or default)
|
|
3074
3205
|
let url;
|
|
3075
3206
|
let method;
|
|
3076
3207
|
let headers;
|
|
3077
|
-
// wrangle fetch input parameters into place
|
|
3078
3208
|
if (input instanceof Request) {
|
|
3079
3209
|
url = new URL(input.url);
|
|
3080
3210
|
method = init?.method || input?.method || 'GET';
|
|
@@ -3087,15 +3217,15 @@ class SessionCore {
|
|
|
3087
3217
|
headers = init.headers ? new Headers(init.headers) : new Headers();
|
|
3088
3218
|
}
|
|
3089
3219
|
// create DPoP token, and add tokens to request
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3220
|
+
if (this.sessionInformation.tokenDetails) {
|
|
3221
|
+
dpopPayload = dpopPayload ?? {
|
|
3222
|
+
htu: `${url.origin}${url.pathname}`,
|
|
3223
|
+
htm: method.toUpperCase()
|
|
3224
|
+
};
|
|
3225
|
+
const dpop = await this.createSignedDPoPToken(dpopPayload);
|
|
3226
|
+
headers.set("dpop", dpop);
|
|
3227
|
+
headers.set("authorization", `DPoP ${this.sessionInformation.tokenDetails.access_token}`);
|
|
3228
|
+
}
|
|
3099
3229
|
// check explicitly; to avoid unexpected behaviour
|
|
3100
3230
|
if (input instanceof Request) { // clone the provided request, and override the headers
|
|
3101
3231
|
return fetch(new Request(input, { ...init, headers }));
|
|
@@ -3103,59 +3233,64 @@ class SessionCore {
|
|
|
3103
3233
|
// just override the headers
|
|
3104
3234
|
return fetch(url, { ...init, headers });
|
|
3105
3235
|
}
|
|
3106
|
-
//
|
|
3107
|
-
// Setters
|
|
3108
|
-
//
|
|
3109
|
-
async setTokenDetails(tokenDetails) {
|
|
3110
|
-
this.information.tokenDetails = tokenDetails;
|
|
3111
|
-
await this._updateSessionDetailsFromToken(tokenDetails.access_token);
|
|
3112
|
-
}
|
|
3113
|
-
clearRefreshPromise() {
|
|
3114
|
-
this.refreshPromise = undefined;
|
|
3115
|
-
this.resolveRefresh = undefined;
|
|
3116
|
-
this.rejectRefresh = undefined;
|
|
3117
|
-
}
|
|
3118
|
-
//
|
|
3119
|
-
// Getters
|
|
3120
|
-
//
|
|
3121
3236
|
get isActive() {
|
|
3122
3237
|
return this.isActive_;
|
|
3123
3238
|
}
|
|
3124
3239
|
get webId() {
|
|
3125
3240
|
return this.webId_;
|
|
3126
3241
|
}
|
|
3127
|
-
getExpiresIn() {
|
|
3128
|
-
return this.information.tokenDetails?.expires_in ?? -1;
|
|
3129
|
-
}
|
|
3130
|
-
isExpired() {
|
|
3131
|
-
if (!this.exp_)
|
|
3132
|
-
return true;
|
|
3133
|
-
return this._isTokenExpired(this.exp_);
|
|
3134
|
-
}
|
|
3135
|
-
getTokenDetails() {
|
|
3136
|
-
return this.information.tokenDetails;
|
|
3137
|
-
}
|
|
3138
3242
|
//
|
|
3139
|
-
//
|
|
3243
|
+
// Helper Methods
|
|
3140
3244
|
//
|
|
3141
3245
|
/**
|
|
3142
|
-
*
|
|
3143
|
-
* and if expired, restore the session.
|
|
3246
|
+
* Set the session to active if there is an access token.
|
|
3144
3247
|
*/
|
|
3145
|
-
async
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3248
|
+
async setSessionDetails() {
|
|
3249
|
+
// check for access token
|
|
3250
|
+
if (!this.sessionInformation.tokenDetails?.access_token) {
|
|
3251
|
+
this.logout();
|
|
3252
|
+
}
|
|
3253
|
+
// generate ath
|
|
3254
|
+
this.currentAth_ = await this.computeAth(this.sessionInformation.tokenDetails.access_token);
|
|
3255
|
+
// check for active session
|
|
3256
|
+
this.webId_ = decodeJwt(this.sessionInformation.tokenDetails.access_token)["webid"];
|
|
3257
|
+
this.isActive_ = this.webId !== undefined;
|
|
3258
|
+
// deactivating session when token expire
|
|
3259
|
+
this.setSessionDeactivateTimeout();
|
|
3260
|
+
// refreshing tokens
|
|
3261
|
+
this.setTokenRefreshTimeout();
|
|
3262
|
+
}
|
|
3263
|
+
setSessionDeactivateTimeout() {
|
|
3264
|
+
const deactivate_buffer_seconds = 5;
|
|
3265
|
+
const timeUntilDeactivate = (this.sessionInformation.tokenDetails.expires_in - deactivate_buffer_seconds) * 1000;
|
|
3266
|
+
if (this.sessionDeactivateTimeout)
|
|
3267
|
+
clearTimeout(this.sessionDeactivateTimeout);
|
|
3268
|
+
this.sessionDeactivateTimeout = setTimeout(() => this.logout(), timeUntilDeactivate);
|
|
3269
|
+
}
|
|
3270
|
+
setTokenRefreshTimeout() {
|
|
3271
|
+
const refresh_buffer_seconds = 95;
|
|
3272
|
+
const timeUntilRefresh = (this.sessionInformation.tokenDetails.expires_in - refresh_buffer_seconds) * 1000;
|
|
3273
|
+
if (this.tokenRefreshTimeout)
|
|
3274
|
+
clearTimeout(this.tokenRefreshTimeout);
|
|
3275
|
+
this.tokenRefreshTimeout = setTimeout(async () => {
|
|
3276
|
+
const newTokens = await renewTokens()
|
|
3277
|
+
.catch((error) => {
|
|
3278
|
+
// anything missing or wrong => could not renew tokens.
|
|
3279
|
+
if (this.onSessionExpirationWarning)
|
|
3280
|
+
this.onSessionExpirationWarning();
|
|
3281
|
+
return undefined;
|
|
3282
|
+
});
|
|
3283
|
+
if (!newTokens) {
|
|
3284
|
+
return;
|
|
3152
3285
|
}
|
|
3153
|
-
|
|
3286
|
+
this.sessionInformation.tokenDetails = newTokens;
|
|
3287
|
+
this.setSessionDetails();
|
|
3288
|
+
}, timeUntilRefresh);
|
|
3154
3289
|
}
|
|
3155
3290
|
/**
|
|
3156
3291
|
* RFC 9449 - Hash of the access token
|
|
3157
3292
|
*/
|
|
3158
|
-
async
|
|
3293
|
+
async computeAth(accessToken) {
|
|
3159
3294
|
// Convert the ASCII string of the token to a Uint8Array
|
|
3160
3295
|
const encoder = new TextEncoder();
|
|
3161
3296
|
const data = encoder.encode(accessToken); // ASCII by default
|
|
@@ -3171,470 +3306,6 @@ class SessionCore {
|
|
|
3171
3306
|
.replace(/=+$/, '');
|
|
3172
3307
|
return base64url;
|
|
3173
3308
|
}
|
|
3174
|
-
/**
|
|
3175
|
-
* Creates a signed DPoP (Demonstration of Proof-of-Possession) token.
|
|
3176
|
-
*
|
|
3177
|
-
* @param payload The payload to include in the DPoP token. By default, it includes `htu` (HTTP target URI) and `htm` (HTTP method).
|
|
3178
|
-
* @returns A promise that resolves to the signed DPoP token string.
|
|
3179
|
-
* @throws Error if the session has not been initialized - if no token details are available.
|
|
3180
|
-
*/
|
|
3181
|
-
async _createSignedDPoPToken(payload) {
|
|
3182
|
-
if (!this.information.tokenDetails || !this.currentAth_) {
|
|
3183
|
-
throw new Error("Session not established.");
|
|
3184
|
-
}
|
|
3185
|
-
payload.ath = this.currentAth_;
|
|
3186
|
-
const jwk_public_key = await exportJWK(this.information.tokenDetails.dpop_key_pair.publicKey);
|
|
3187
|
-
return new SignJWT(payload)
|
|
3188
|
-
.setIssuedAt()
|
|
3189
|
-
.setJti(window.crypto.randomUUID())
|
|
3190
|
-
.setProtectedHeader({
|
|
3191
|
-
alg: "ES256",
|
|
3192
|
-
typ: "dpop+jwt",
|
|
3193
|
-
jwk: jwk_public_key,
|
|
3194
|
-
})
|
|
3195
|
-
.sign(this.information.tokenDetails.dpop_key_pair.privateKey);
|
|
3196
|
-
}
|
|
3197
|
-
async _updateSessionDetailsFromToken(access_token) {
|
|
3198
|
-
if (!access_token) {
|
|
3199
|
-
await this.logout();
|
|
3200
|
-
return;
|
|
3201
|
-
}
|
|
3202
|
-
try {
|
|
3203
|
-
const decodedToken = decodeJwt(access_token);
|
|
3204
|
-
const webId = decodedToken.webid;
|
|
3205
|
-
if (!webId) {
|
|
3206
|
-
throw new Error('Missing webid claim in access token');
|
|
3207
|
-
}
|
|
3208
|
-
const exp = decodedToken.exp;
|
|
3209
|
-
if (!exp) {
|
|
3210
|
-
throw new Error('Missing exp claim in access token');
|
|
3211
|
-
}
|
|
3212
|
-
this.currentAth_ = await this._computeAth(access_token); // must be done before session set to active
|
|
3213
|
-
this.webId_ = webId;
|
|
3214
|
-
this.exp_ = exp;
|
|
3215
|
-
this.isActive_ = true;
|
|
3216
|
-
}
|
|
3217
|
-
catch (error) {
|
|
3218
|
-
await this.logout();
|
|
3219
|
-
}
|
|
3220
|
-
}
|
|
3221
|
-
/**
|
|
3222
|
-
* Checks if a JWT expiration timestamp ('exp') has passed.
|
|
3223
|
-
*/
|
|
3224
|
-
_isTokenExpired(exp, bufferSeconds = 0) {
|
|
3225
|
-
if (typeof exp !== 'number' || isNaN(exp)) {
|
|
3226
|
-
return true;
|
|
3227
|
-
}
|
|
3228
|
-
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
|
3229
|
-
return exp < (currentTimeSeconds + bufferSeconds);
|
|
3230
|
-
}
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
// extracting this from Session.ts such that the jest tests would compile :)
|
|
3234
|
-
const getWorkerUrl = () => new URL('./RefreshWorker.js', (typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('ion-icon.ion-progress-bar.ion-skeleton-text.pos-add-new-thing.pos-app.pos-app-browser.pos-app-dashboard.pos-app-settings.pos-description.pos-dialog.pos-error-toast.pos-example-resources.pos-getting-started.pos-image.pos-internal-router.pos-label.pos-login.pos-login-form.pos-make-findable.pos-navigation.pos-navigation-bar.pos-new-thing-form.pos-picture.pos-resource.pos-rich-link.pos-router.pos-select-term.pos-setting-offline-cache.pos-tool-select.pos-type-router.pos-user-menu.entry.cjs.js', document.baseURI).href)));
|
|
3235
|
-
|
|
3236
|
-
/**
|
|
3237
|
-
* A simple IndexedDB wrapper.
|
|
3238
|
-
*/
|
|
3239
|
-
class SessionIDB {
|
|
3240
|
-
dbName;
|
|
3241
|
-
storeName;
|
|
3242
|
-
dbVersion;
|
|
3243
|
-
db = null;
|
|
3244
|
-
/**
|
|
3245
|
-
* Creates a new instance
|
|
3246
|
-
* @param dbName The name of the IndexedDB database
|
|
3247
|
-
* @param storeName The name of the object store
|
|
3248
|
-
* @param dbVersion The database version
|
|
3249
|
-
*/
|
|
3250
|
-
constructor(dbName = 'soidc', storeName = 'session', dbVersion = 1) {
|
|
3251
|
-
this.dbName = dbName;
|
|
3252
|
-
this.storeName = storeName;
|
|
3253
|
-
this.dbVersion = dbVersion;
|
|
3254
|
-
}
|
|
3255
|
-
/**
|
|
3256
|
-
* Initializes the IndexedDB database
|
|
3257
|
-
* @returns Promise that resolves when the database is ready
|
|
3258
|
-
*/
|
|
3259
|
-
async init() {
|
|
3260
|
-
return new Promise((resolve, reject) => {
|
|
3261
|
-
const request = indexedDB.open(this.dbName, this.dbVersion);
|
|
3262
|
-
request.onerror = (event) => {
|
|
3263
|
-
reject(new Error(`Database error: ${event.target.error}`));
|
|
3264
|
-
};
|
|
3265
|
-
request.onsuccess = (event) => {
|
|
3266
|
-
this.db = event.target.result;
|
|
3267
|
-
resolve(this);
|
|
3268
|
-
};
|
|
3269
|
-
request.onupgradeneeded = (event) => {
|
|
3270
|
-
const db = event.target.result;
|
|
3271
|
-
// Check if the object store already exists, if not create it
|
|
3272
|
-
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
3273
|
-
db.createObjectStore(this.storeName);
|
|
3274
|
-
}
|
|
3275
|
-
};
|
|
3276
|
-
});
|
|
3277
|
-
}
|
|
3278
|
-
/**
|
|
3279
|
-
* Stores any value in the database with the given ID as key
|
|
3280
|
-
* @param id The identifier/key for the value
|
|
3281
|
-
* @param value The value to store
|
|
3282
|
-
*/
|
|
3283
|
-
async setItem(id, value) {
|
|
3284
|
-
if (!this.db) {
|
|
3285
|
-
await this.init();
|
|
3286
|
-
}
|
|
3287
|
-
return new Promise((resolve, reject) => {
|
|
3288
|
-
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
3289
|
-
// Handle transation
|
|
3290
|
-
transaction.oncomplete = () => {
|
|
3291
|
-
resolve();
|
|
3292
|
-
};
|
|
3293
|
-
transaction.onerror = (event) => {
|
|
3294
|
-
reject(new Error(`Transaction error for setItem(${id},...): ${event.target.error}`));
|
|
3295
|
-
};
|
|
3296
|
-
transaction.onabort = (event) => {
|
|
3297
|
-
reject(new Error(`Transaction aborted for setItem(${id},...): ${event.target.error}`));
|
|
3298
|
-
};
|
|
3299
|
-
// Perform the request within the transaction
|
|
3300
|
-
const store = transaction.objectStore(this.storeName);
|
|
3301
|
-
store.put(value, id);
|
|
3302
|
-
});
|
|
3303
|
-
}
|
|
3304
|
-
/**
|
|
3305
|
-
* Retrieves a value from the database by ID
|
|
3306
|
-
* @param id The identifier/key for the value
|
|
3307
|
-
* @returns The stored value or null if not found
|
|
3308
|
-
*/
|
|
3309
|
-
async getItem(id) {
|
|
3310
|
-
if (!this.db) {
|
|
3311
|
-
await this.init();
|
|
3312
|
-
}
|
|
3313
|
-
return new Promise((resolve, reject) => {
|
|
3314
|
-
const transaction = this.db.transaction(this.storeName, 'readonly');
|
|
3315
|
-
// Handle transation
|
|
3316
|
-
transaction.onerror = (event) => {
|
|
3317
|
-
reject(new Error(`Transaction error for getItem(${id}): ${event.target.error}`));
|
|
3318
|
-
};
|
|
3319
|
-
transaction.onabort = (event) => {
|
|
3320
|
-
reject(new Error(`Transaction aborted for getItem(${id}): ${event.target.error}`));
|
|
3321
|
-
};
|
|
3322
|
-
// Perform the request within the transaction
|
|
3323
|
-
const store = transaction.objectStore(this.storeName);
|
|
3324
|
-
const request = store.get(id);
|
|
3325
|
-
request.onsuccess = () => {
|
|
3326
|
-
resolve(request.result || null);
|
|
3327
|
-
};
|
|
3328
|
-
});
|
|
3329
|
-
}
|
|
3330
|
-
/**
|
|
3331
|
-
* Removes an item from the database
|
|
3332
|
-
* @param id The identifier of the item to remove
|
|
3333
|
-
*/
|
|
3334
|
-
async deleteItem(id) {
|
|
3335
|
-
if (!this.db) {
|
|
3336
|
-
await this.init();
|
|
3337
|
-
}
|
|
3338
|
-
return new Promise((resolve, reject) => {
|
|
3339
|
-
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
3340
|
-
// Handle transation
|
|
3341
|
-
transaction.oncomplete = () => {
|
|
3342
|
-
resolve();
|
|
3343
|
-
};
|
|
3344
|
-
transaction.onerror = (event) => {
|
|
3345
|
-
reject(new Error(`Transaction error for deleteItem(${id}): ${event.target.error}`));
|
|
3346
|
-
};
|
|
3347
|
-
transaction.onabort = (event) => {
|
|
3348
|
-
reject(new Error(`Transaction aborted for deleteItem(${id}): ${event.target.error}`));
|
|
3349
|
-
};
|
|
3350
|
-
// Perform the request within the transaction
|
|
3351
|
-
const store = transaction.objectStore(this.storeName);
|
|
3352
|
-
store.delete(id);
|
|
3353
|
-
});
|
|
3354
|
-
}
|
|
3355
|
-
/**
|
|
3356
|
-
* Clears all items from the database
|
|
3357
|
-
*/
|
|
3358
|
-
async clear() {
|
|
3359
|
-
if (!this.db) {
|
|
3360
|
-
await this.init();
|
|
3361
|
-
}
|
|
3362
|
-
return new Promise((resolve, reject) => {
|
|
3363
|
-
const transaction = this.db.transaction(this.storeName, 'readwrite');
|
|
3364
|
-
// Handle transation
|
|
3365
|
-
transaction.oncomplete = () => {
|
|
3366
|
-
resolve();
|
|
3367
|
-
};
|
|
3368
|
-
transaction.onerror = (event) => {
|
|
3369
|
-
reject(new Error(`Transaction error for clear(): ${event.target.error}`));
|
|
3370
|
-
};
|
|
3371
|
-
transaction.onabort = (event) => {
|
|
3372
|
-
reject(new Error(`Transaction aborted for clear(): ${event.target.error}`));
|
|
3373
|
-
};
|
|
3374
|
-
// Perform the request within the transaction
|
|
3375
|
-
const store = transaction.objectStore(this.storeName);
|
|
3376
|
-
store.clear();
|
|
3377
|
-
});
|
|
3378
|
-
}
|
|
3379
|
-
/**
|
|
3380
|
-
* Closes the database connection
|
|
3381
|
-
*/
|
|
3382
|
-
close() {
|
|
3383
|
-
if (this.db) {
|
|
3384
|
-
this.db.close();
|
|
3385
|
-
this.db = null;
|
|
3386
|
-
}
|
|
3387
|
-
}
|
|
3388
|
-
}
|
|
3389
|
-
|
|
3390
|
-
var RefreshMessageTypes;
|
|
3391
|
-
(function (RefreshMessageTypes) {
|
|
3392
|
-
RefreshMessageTypes["SCHEDULE"] = "SCHEDULE";
|
|
3393
|
-
RefreshMessageTypes["REFRESH"] = "REFRESH";
|
|
3394
|
-
RefreshMessageTypes["STOP"] = "STOP";
|
|
3395
|
-
RefreshMessageTypes["DISCONNECT"] = "DISCONNECT";
|
|
3396
|
-
RefreshMessageTypes["TOKEN_DETAILS"] = "TOKEN_DETAILS";
|
|
3397
|
-
RefreshMessageTypes["ERROR_ON_REFRESH"] = "ERROR_ON_REFRESH";
|
|
3398
|
-
RefreshMessageTypes["EXPIRED"] = "EXPIRED";
|
|
3399
|
-
})(RefreshMessageTypes || (RefreshMessageTypes = {}));
|
|
3400
|
-
// A Set to store all connected ports (tabs)
|
|
3401
|
-
const ports = new Set();
|
|
3402
|
-
const broadcast = (message) => {
|
|
3403
|
-
for (const p of ports) {
|
|
3404
|
-
p.postMessage(message);
|
|
3405
|
-
}
|
|
3406
|
-
};
|
|
3407
|
-
let refresher;
|
|
3408
|
-
self.onconnect = (event) => {
|
|
3409
|
-
const port = event.ports[0];
|
|
3410
|
-
ports.add(port);
|
|
3411
|
-
// lazy init
|
|
3412
|
-
if (!refresher) {
|
|
3413
|
-
refresher = new Refresher(broadcast, new SessionIDB());
|
|
3414
|
-
}
|
|
3415
|
-
// handle messages
|
|
3416
|
-
port.onmessage = (event) => {
|
|
3417
|
-
const { type, payload } = event.data;
|
|
3418
|
-
switch (type) {
|
|
3419
|
-
case RefreshMessageTypes.SCHEDULE:
|
|
3420
|
-
refresher.handleSchedule(payload);
|
|
3421
|
-
break;
|
|
3422
|
-
case RefreshMessageTypes.REFRESH:
|
|
3423
|
-
refresher.handleRefresh(port);
|
|
3424
|
-
break;
|
|
3425
|
-
case RefreshMessageTypes.STOP:
|
|
3426
|
-
refresher.handleStop();
|
|
3427
|
-
break;
|
|
3428
|
-
case RefreshMessageTypes.DISCONNECT:
|
|
3429
|
-
ports.delete(port);
|
|
3430
|
-
break;
|
|
3431
|
-
}
|
|
3432
|
-
};
|
|
3433
|
-
port.onmessageerror = () => ports.delete(port);
|
|
3434
|
-
port.start();
|
|
3435
|
-
};
|
|
3436
|
-
class Refresher {
|
|
3437
|
-
tokenDetails;
|
|
3438
|
-
exp;
|
|
3439
|
-
refreshTimeout;
|
|
3440
|
-
finalLogoutTimeout;
|
|
3441
|
-
timersAreRunning = false;
|
|
3442
|
-
broadcast;
|
|
3443
|
-
database;
|
|
3444
|
-
refreshPromise;
|
|
3445
|
-
constructor(broadcast, database) {
|
|
3446
|
-
this.broadcast = broadcast;
|
|
3447
|
-
this.database = database;
|
|
3448
|
-
}
|
|
3449
|
-
async handleSchedule(tokenDetails) {
|
|
3450
|
-
this.tokenDetails = tokenDetails;
|
|
3451
|
-
this.exp = decodeJwt(this.tokenDetails.access_token).exp;
|
|
3452
|
-
this.broadcast({
|
|
3453
|
-
type: RefreshMessageTypes.TOKEN_DETAILS,
|
|
3454
|
-
payload: { tokenDetails: this.tokenDetails }
|
|
3455
|
-
});
|
|
3456
|
-
console.log(`[RefreshWorker] Scheduling timers, expiry in ${this.tokenDetails.expires_in}s`);
|
|
3457
|
-
this.scheduleTimers(this.tokenDetails.expires_in);
|
|
3458
|
-
this.timersAreRunning = true;
|
|
3459
|
-
}
|
|
3460
|
-
async handleRefresh(requestingPort) {
|
|
3461
|
-
if (this.tokenDetails && this.exp && !this.isTokenExpired(this.exp)) {
|
|
3462
|
-
console.log(`[RefreshWorker] Providing current tokens`);
|
|
3463
|
-
requestingPort.postMessage({
|
|
3464
|
-
type: RefreshMessageTypes.TOKEN_DETAILS,
|
|
3465
|
-
payload: { tokenDetails: this.tokenDetails }
|
|
3466
|
-
});
|
|
3467
|
-
}
|
|
3468
|
-
else {
|
|
3469
|
-
console.log(`[RefreshWorker] Refreshing tokens`);
|
|
3470
|
-
this.performRefresh();
|
|
3471
|
-
}
|
|
3472
|
-
}
|
|
3473
|
-
handleStop() {
|
|
3474
|
-
if (!this.tokenDetails) {
|
|
3475
|
-
console.log('[RefreshWorker] Received STOP, being idle');
|
|
3476
|
-
return;
|
|
3477
|
-
}
|
|
3478
|
-
this.broadcast({ type: RefreshMessageTypes.EXPIRED });
|
|
3479
|
-
this.tokenDetails = undefined;
|
|
3480
|
-
this.exp = undefined;
|
|
3481
|
-
this.refreshPromise = undefined;
|
|
3482
|
-
console.log('[RefreshWorker] Received STOP, clearing timers');
|
|
3483
|
-
this.clearAllTimers();
|
|
3484
|
-
}
|
|
3485
|
-
async performRefresh() {
|
|
3486
|
-
if (this.refreshPromise) {
|
|
3487
|
-
console.log('[RefreshWorker] Refresh already in progress, waiting...');
|
|
3488
|
-
return this.refreshPromise;
|
|
3489
|
-
}
|
|
3490
|
-
this.refreshPromise = this.doRefresh();
|
|
3491
|
-
return this.refreshPromise;
|
|
3492
|
-
}
|
|
3493
|
-
async doRefresh() {
|
|
3494
|
-
try {
|
|
3495
|
-
this.tokenDetails = await renewTokens(this.database);
|
|
3496
|
-
this.exp = decodeJwt(this.tokenDetails.access_token).exp;
|
|
3497
|
-
this.broadcast({
|
|
3498
|
-
type: RefreshMessageTypes.TOKEN_DETAILS,
|
|
3499
|
-
payload: { tokenDetails: this.tokenDetails }
|
|
3500
|
-
});
|
|
3501
|
-
console.log(`[RefreshWorker] Token refreshed`);
|
|
3502
|
-
console.log(`[RefreshWorker] Scheduling timers, expiry in ${this.tokenDetails.expires_in}s`);
|
|
3503
|
-
this.scheduleTimers(this.tokenDetails.expires_in);
|
|
3504
|
-
}
|
|
3505
|
-
catch (error) {
|
|
3506
|
-
this.broadcast({
|
|
3507
|
-
type: RefreshMessageTypes.ERROR_ON_REFRESH,
|
|
3508
|
-
error: error.message
|
|
3509
|
-
});
|
|
3510
|
-
console.log(`[RefreshWorker]`, error.message);
|
|
3511
|
-
}
|
|
3512
|
-
finally {
|
|
3513
|
-
this.refreshPromise = undefined;
|
|
3514
|
-
}
|
|
3515
|
-
}
|
|
3516
|
-
clearAllTimers() {
|
|
3517
|
-
if (this.refreshTimeout)
|
|
3518
|
-
clearTimeout(this.refreshTimeout);
|
|
3519
|
-
if (this.finalLogoutTimeout)
|
|
3520
|
-
clearTimeout(this.finalLogoutTimeout);
|
|
3521
|
-
this.timersAreRunning = false;
|
|
3522
|
-
}
|
|
3523
|
-
scheduleTimers(expiresIn) {
|
|
3524
|
-
this.clearAllTimers();
|
|
3525
|
-
this.timersAreRunning = true;
|
|
3526
|
-
const expiresInMs = expiresIn * 1000;
|
|
3527
|
-
const REFRESH_THRESHOLD_RATIO = 0.8;
|
|
3528
|
-
const MINIMUM_REFRESH_BUFFER_MS = 30 * 1000;
|
|
3529
|
-
const timeUntilRefresh = REFRESH_THRESHOLD_RATIO * expiresInMs;
|
|
3530
|
-
if (timeUntilRefresh > MINIMUM_REFRESH_BUFFER_MS) {
|
|
3531
|
-
this.refreshTimeout = setTimeout(() => this.performRefresh(), timeUntilRefresh);
|
|
3532
|
-
}
|
|
3533
|
-
const LOGOUT_WARNING_BUFFER_MS = 5 * 1000;
|
|
3534
|
-
const timeUntilLogout = expiresInMs - LOGOUT_WARNING_BUFFER_MS;
|
|
3535
|
-
this.finalLogoutTimeout = setTimeout(() => {
|
|
3536
|
-
this.tokenDetails = undefined;
|
|
3537
|
-
this.broadcast({ type: RefreshMessageTypes.EXPIRED });
|
|
3538
|
-
}, timeUntilLogout);
|
|
3539
|
-
}
|
|
3540
|
-
isTokenExpired(exp, bufferSeconds = 0) {
|
|
3541
|
-
if (typeof exp !== 'number' || isNaN(exp)) {
|
|
3542
|
-
return true;
|
|
3543
|
-
}
|
|
3544
|
-
const currentTimeSeconds = Math.floor(Date.now() / 1000);
|
|
3545
|
-
return exp < (currentTimeSeconds + bufferSeconds);
|
|
3546
|
-
}
|
|
3547
|
-
setTokenDetails(tokenDetails) {
|
|
3548
|
-
this.tokenDetails = tokenDetails;
|
|
3549
|
-
}
|
|
3550
|
-
// For testing
|
|
3551
|
-
getTimersAreRunning() { return this.timersAreRunning; }
|
|
3552
|
-
getTokenDetails() { return this.tokenDetails; }
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
/**
|
|
3556
|
-
* This Session provides background token refreshing using a Web Worker.
|
|
3557
|
-
*/
|
|
3558
|
-
class WebWorkerSession extends SessionCore {
|
|
3559
|
-
worker;
|
|
3560
|
-
onSessionExpirationWarning;
|
|
3561
|
-
onSessionExpiration;
|
|
3562
|
-
constructor(clientDetails, sessionOptions) {
|
|
3563
|
-
const database = new SessionIDB();
|
|
3564
|
-
const options = { ...sessionOptions, database };
|
|
3565
|
-
super(clientDetails, options);
|
|
3566
|
-
this.onSessionExpirationWarning = sessionOptions?.onSessionExpirationWarning;
|
|
3567
|
-
this.onSessionExpiration = sessionOptions?.onSessionExpiration;
|
|
3568
|
-
// Allow consumer to provide worker URL, or use default
|
|
3569
|
-
const workerUrl = sessionOptions?.workerUrl ?? getWorkerUrl();
|
|
3570
|
-
this.worker = new SharedWorker(workerUrl, { type: 'module' });
|
|
3571
|
-
this.worker.port.onmessage = (event) => {
|
|
3572
|
-
this.handleWorkerMessage(event.data).catch(console.error);
|
|
3573
|
-
};
|
|
3574
|
-
window.addEventListener('beforeunload', () => {
|
|
3575
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.DISCONNECT });
|
|
3576
|
-
});
|
|
3577
|
-
}
|
|
3578
|
-
async handleWorkerMessage(data) {
|
|
3579
|
-
const { type, payload, error } = data;
|
|
3580
|
-
switch (type) {
|
|
3581
|
-
case RefreshMessageTypes.TOKEN_DETAILS:
|
|
3582
|
-
const wasActive = this.isActive;
|
|
3583
|
-
await this.setTokenDetails(payload.tokenDetails);
|
|
3584
|
-
if (wasActive !== this.isActive)
|
|
3585
|
-
this.onSessionStateChange?.();
|
|
3586
|
-
if (this.refreshPromise && this.resolveRefresh) {
|
|
3587
|
-
this.resolveRefresh();
|
|
3588
|
-
this.clearRefreshPromise();
|
|
3589
|
-
}
|
|
3590
|
-
break;
|
|
3591
|
-
case RefreshMessageTypes.ERROR_ON_REFRESH:
|
|
3592
|
-
if (this.isActive)
|
|
3593
|
-
this.onSessionExpirationWarning?.();
|
|
3594
|
-
if (this.refreshPromise && this.rejectRefresh) {
|
|
3595
|
-
if (this.isActive) {
|
|
3596
|
-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
|
|
3597
|
-
}
|
|
3598
|
-
else {
|
|
3599
|
-
this.rejectRefresh(new Error("No session to restore"));
|
|
3600
|
-
}
|
|
3601
|
-
this.clearRefreshPromise();
|
|
3602
|
-
}
|
|
3603
|
-
break;
|
|
3604
|
-
case RefreshMessageTypes.EXPIRED:
|
|
3605
|
-
if (this.isActive) {
|
|
3606
|
-
this.onSessionExpiration?.();
|
|
3607
|
-
await this.logout();
|
|
3608
|
-
}
|
|
3609
|
-
if (this.refreshPromise && this.rejectRefresh) {
|
|
3610
|
-
this.rejectRefresh(new Error(error || 'Token refresh failed'));
|
|
3611
|
-
this.clearRefreshPromise();
|
|
3612
|
-
}
|
|
3613
|
-
break;
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
;
|
|
3617
|
-
async handleRedirectFromLogin() {
|
|
3618
|
-
await super.handleRedirectFromLogin();
|
|
3619
|
-
if (this.isActive) { // If login was successful, tell the worker to schedule refreshing
|
|
3620
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.SCHEDULE, payload: this.getTokenDetails() });
|
|
3621
|
-
}
|
|
3622
|
-
}
|
|
3623
|
-
async restore() {
|
|
3624
|
-
if (this.refreshPromise) {
|
|
3625
|
-
return this.refreshPromise;
|
|
3626
|
-
}
|
|
3627
|
-
this.refreshPromise = new Promise((resolve, reject) => {
|
|
3628
|
-
this.resolveRefresh = resolve;
|
|
3629
|
-
this.rejectRefresh = reject;
|
|
3630
|
-
});
|
|
3631
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.REFRESH });
|
|
3632
|
-
return this.refreshPromise;
|
|
3633
|
-
}
|
|
3634
|
-
async logout() {
|
|
3635
|
-
this.worker.port.postMessage({ type: RefreshMessageTypes.STOP });
|
|
3636
|
-
await super.logout();
|
|
3637
|
-
}
|
|
3638
3309
|
}
|
|
3639
3310
|
|
|
3640
3311
|
class BrowserSession {
|
|
@@ -3647,7 +3318,7 @@ class BrowserSession {
|
|
|
3647
3318
|
isLoggedIn: false,
|
|
3648
3319
|
webId: undefined,
|
|
3649
3320
|
});
|
|
3650
|
-
this.session = new
|
|
3321
|
+
this.session = new Session();
|
|
3651
3322
|
this._authenticatedFetch = this.session.authFetch.bind(this.session);
|
|
3652
3323
|
}
|
|
3653
3324
|
async handleIncomingRedirect(restorePreviousSession = false) {
|