@inrupt/solid-client-authn-browser 2.3.0 → 2.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/dist/index.js +274 -238
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +274 -238
- package/dist/index.mjs.map +1 -1
- package/dist/solid-client-authn.bundle.js +7 -7
- package/dist/solid-client-authn.bundle.js.map +1 -1
- package/package.json +6 -6
- package/src/ClientAuthentication.spec.ts +29 -0
- package/src/ClientAuthentication.ts +5 -6
- package/src/Session.spec.ts +1 -1
- package/src/login/oidc/ClientRegistrar.spec.ts +154 -78
- package/src/login/oidc/ClientRegistrar.ts +33 -10
- package/src/login/oidc/Redirector.spec.ts +2 -2
- package/src/login/oidc/__mocks__/ClientRegistrar.ts +2 -0
- package/src/login/oidc/incomingRedirectHandler/AuthCodeRedirectHandler.ts +1 -1
- package/src/login/oidc/oidcHandlers/AuthorizationCodeWithPkceOidcHandler.spec.ts +1 -0
- package/tsconfig.json +1 -1
package/dist/index.js
CHANGED
|
@@ -62,82 +62,77 @@ class StorageUtilityBrowser extends solidClientAuthnCore.StorageUtility {
|
|
|
62
62
|
* @hidden
|
|
63
63
|
*/
|
|
64
64
|
class ClientAuthentication extends solidClientAuthnCore.ClientAuthentication {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
//
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// on login.
|
|
73
|
-
// But we may want to preserve our client application info, particularly if
|
|
74
|
-
// we used Dynamic Client Registration to register (since we don't
|
|
75
|
-
// necessarily want the user to have to register this app each time they
|
|
76
|
-
// login).
|
|
65
|
+
// Define these functions as properties so that they don't get accidentally re-bound.
|
|
66
|
+
// Isn't Javascript fun?
|
|
67
|
+
login = async (options, eventEmitter) => {
|
|
68
|
+
// In order to get a clean start, make sure that the session is logged out
|
|
69
|
+
// on login, except when doing a silent login so that Dynamic Client information
|
|
70
|
+
// is preserved.
|
|
71
|
+
if (options.prompt !== "none") {
|
|
77
72
|
await this.sessionInfoManager.clear(options.sessionId);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
}
|
|
73
|
+
}
|
|
74
|
+
// In the case of the user hitting the 'back' button in their browser, they
|
|
75
|
+
// could return to a previous redirect URL that contains OIDC params that
|
|
76
|
+
// are now longer valid. To be safe, strip relevant params now.
|
|
77
|
+
// If the user is providing a redirect IRI, it should not be modified, so
|
|
78
|
+
// normalization only applies if we default to the current location (which is
|
|
79
|
+
// a bad practice and should be discouraged).
|
|
80
|
+
const redirectUrl = options.redirectUrl ?? oidcClientExt.normalizeCallbackUrl(window.location.href);
|
|
81
|
+
if (!solidClientAuthnCore.isValidRedirectUrl(redirectUrl)) {
|
|
82
|
+
throw new Error(`${redirectUrl} is not a valid redirect URL, it is either a malformed IRI, includes a hash fragment, or reserved query parameters ('code' or 'state').`);
|
|
83
|
+
}
|
|
84
|
+
await this.loginHandler.handle({
|
|
85
|
+
...options,
|
|
86
|
+
redirectUrl,
|
|
87
|
+
// If no clientName is provided, the clientId may be used instead.
|
|
88
|
+
clientName: options.clientName ?? options.clientId,
|
|
89
|
+
eventEmitter,
|
|
90
|
+
});
|
|
91
|
+
};
|
|
92
|
+
// Collects session information from storage, and returns them. Returns null
|
|
93
|
+
// if the expected information cannot be found.
|
|
94
|
+
// Note that the ID token is not stored, which means the session information
|
|
95
|
+
// cannot be validated at this point.
|
|
96
|
+
validateCurrentSession = async (currentSessionId) => {
|
|
97
|
+
const sessionInfo = await this.sessionInfoManager.get(currentSessionId);
|
|
98
|
+
if (sessionInfo === undefined ||
|
|
99
|
+
sessionInfo.clientAppId === undefined ||
|
|
100
|
+
sessionInfo.issuer === undefined) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return sessionInfo;
|
|
104
|
+
};
|
|
105
|
+
handleIncomingRedirect = async (url, eventEmitter) => {
|
|
106
|
+
try {
|
|
107
|
+
const redirectInfo = await this.redirectHandler.handle(url, eventEmitter, undefined);
|
|
108
|
+
// The `FallbackRedirectHandler` directly returns the global `fetch` for
|
|
109
|
+
// his value, so we should ensure it's bound to `window` rather than to
|
|
110
|
+
// ClientAuthentication, to avoid the following error:
|
|
111
|
+
// > 'fetch' called on an object that does not implement interface Window.
|
|
112
|
+
this.fetch = redirectInfo.fetch.bind(window);
|
|
113
|
+
this.boundLogout = redirectInfo.getLogoutUrl;
|
|
114
|
+
// Strip the oauth params:
|
|
115
|
+
await this.cleanUrlAfterRedirect(url);
|
|
116
|
+
return {
|
|
117
|
+
isLoggedIn: redirectInfo.isLoggedIn,
|
|
118
|
+
webId: redirectInfo.webId,
|
|
119
|
+
sessionId: redirectInfo.sessionId,
|
|
120
|
+
expirationDate: redirectInfo.expirationDate,
|
|
121
|
+
clientAppId: redirectInfo.clientAppId,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
// Strip the oauth params:
|
|
126
|
+
await this.cleanUrlAfterRedirect(url);
|
|
127
|
+
// FIXME: EVENTS.ERROR should be errorCode, errorDescription
|
|
128
|
+
//
|
|
129
|
+
// I'm not sure if "redirect" is a good error code, and in theory `err`
|
|
130
|
+
// maybe an Error object and not a string; Maybe we want to just hardcode
|
|
131
|
+
// a description instead?
|
|
132
|
+
eventEmitter.emit(solidClientAuthnCore.EVENTS.ERROR, "redirect", err);
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
141
136
|
async cleanUrlAfterRedirect(url) {
|
|
142
137
|
const cleanedUpUrl = solidClientAuthnCore.removeOpenIdParams(url).href;
|
|
143
138
|
// Remove OAuth-specific query params (since the login flow finishes with
|
|
@@ -190,6 +185,10 @@ function hasRedirectUrl(options) {
|
|
|
190
185
|
* @hidden
|
|
191
186
|
*/
|
|
192
187
|
class OidcLoginHandler {
|
|
188
|
+
storageUtility;
|
|
189
|
+
oidcHandler;
|
|
190
|
+
issuerConfigFetcher;
|
|
191
|
+
clientRegistrar;
|
|
193
192
|
constructor(storageUtility, oidcHandler, issuerConfigFetcher, clientRegistrar) {
|
|
194
193
|
this.storageUtility = storageUtility;
|
|
195
194
|
this.oidcHandler = oidcHandler;
|
|
@@ -259,7 +258,6 @@ class OidcLoginHandler {
|
|
|
259
258
|
*/
|
|
260
259
|
class AuthorizationCodeWithPkceOidcHandler extends solidClientAuthnCore.AuthorizationCodeWithPkceOidcHandlerBase {
|
|
261
260
|
async handle(oidcLoginOptions) {
|
|
262
|
-
var _a;
|
|
263
261
|
/* eslint-disable camelcase */
|
|
264
262
|
const oidcOptions = {
|
|
265
263
|
authority: oidcLoginOptions.issuer.toString(),
|
|
@@ -274,7 +272,7 @@ class AuthorizationCodeWithPkceOidcHandler extends solidClientAuthnCore.Authoriz
|
|
|
274
272
|
// profile referenced by the WebId.
|
|
275
273
|
loadUserInfo: false,
|
|
276
274
|
code_verifier: true,
|
|
277
|
-
prompt:
|
|
275
|
+
prompt: oidcLoginOptions.prompt ?? "consent",
|
|
278
276
|
};
|
|
279
277
|
/* eslint-enable camelcase */
|
|
280
278
|
const oidcClientLibrary = new oidcClientExt.OidcClient(oidcOptions);
|
|
@@ -429,6 +427,7 @@ function processConfig(config) {
|
|
|
429
427
|
* @hidden
|
|
430
428
|
*/
|
|
431
429
|
class IssuerConfigFetcher {
|
|
430
|
+
storageUtility;
|
|
432
431
|
constructor(storageUtility) {
|
|
433
432
|
this.storageUtility = storageUtility;
|
|
434
433
|
this.storageUtility = storageUtility;
|
|
@@ -547,7 +546,7 @@ class SessionInfoManager extends solidClientAuthnCore.SessionInfoManagerBase {
|
|
|
547
546
|
clientAppId: clientId,
|
|
548
547
|
clientAppSecret: clientSecret,
|
|
549
548
|
// Default the token type to DPoP if unspecified.
|
|
550
|
-
tokenType: tokenType
|
|
549
|
+
tokenType: tokenType ?? "DPoP",
|
|
551
550
|
};
|
|
552
551
|
}
|
|
553
552
|
/**
|
|
@@ -630,6 +629,11 @@ class FallbackRedirectHandler {
|
|
|
630
629
|
* @hidden
|
|
631
630
|
*/
|
|
632
631
|
class AuthCodeRedirectHandler {
|
|
632
|
+
storageUtility;
|
|
633
|
+
sessionInfoManager;
|
|
634
|
+
issuerConfigFetcher;
|
|
635
|
+
clientRegistrar;
|
|
636
|
+
tokerRefresher;
|
|
633
637
|
constructor(storageUtility, sessionInfoManager, issuerConfigFetcher, clientRegistrar, tokerRefresher) {
|
|
634
638
|
this.storageUtility = storageUtility;
|
|
635
639
|
this.sessionInfoManager = sessionInfoManager;
|
|
@@ -693,7 +697,7 @@ class AuthCodeRedirectHandler {
|
|
|
693
697
|
tokenRefresher: this.tokerRefresher,
|
|
694
698
|
};
|
|
695
699
|
}
|
|
696
|
-
const authFetch =
|
|
700
|
+
const authFetch = solidClientAuthnCore.buildAuthenticatedFetch(tokens.accessToken, {
|
|
697
701
|
dpopKey: tokens.dpopKey,
|
|
698
702
|
refreshOptions,
|
|
699
703
|
eventEmitter,
|
|
@@ -845,19 +849,23 @@ class Redirector {
|
|
|
845
849
|
* @hidden
|
|
846
850
|
*/
|
|
847
851
|
class ClientRegistrar {
|
|
852
|
+
storageUtility;
|
|
848
853
|
constructor(storageUtility) {
|
|
849
854
|
this.storageUtility = storageUtility;
|
|
850
855
|
this.storageUtility = storageUtility;
|
|
851
856
|
}
|
|
852
857
|
async getClient(options, issuerConfig) {
|
|
853
858
|
// If client secret and/or client id are stored in storage, use those.
|
|
854
|
-
const [storedClientId, storedClientSecret, storedClientName, storedClientType,] = await Promise.all([
|
|
859
|
+
const [storedClientId, storedClientSecret, expiresAt, storedClientName, storedClientType,] = await Promise.all([
|
|
855
860
|
this.storageUtility.getForUser(options.sessionId, "clientId", {
|
|
856
861
|
secure: false,
|
|
857
862
|
}),
|
|
858
863
|
this.storageUtility.getForUser(options.sessionId, "clientSecret", {
|
|
859
864
|
secure: false,
|
|
860
865
|
}),
|
|
866
|
+
this.storageUtility.getForUser(options.sessionId, "expiresAt", {
|
|
867
|
+
secure: false,
|
|
868
|
+
}),
|
|
861
869
|
this.storageUtility.getForUser(options.sessionId, "clientName", {
|
|
862
870
|
secure: false,
|
|
863
871
|
}),
|
|
@@ -865,14 +873,30 @@ class ClientRegistrar {
|
|
|
865
873
|
secure: false,
|
|
866
874
|
}),
|
|
867
875
|
]);
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
+
// -1 is used as a default to identify legacy cases when a value should
|
|
877
|
+
// have been stored, but wasn't. It will be treated as an expired client.
|
|
878
|
+
const expirationDate = expiresAt !== undefined ? Number.parseInt(expiresAt, 10) : -1;
|
|
879
|
+
// Expiration is only applicable to confidential clients.
|
|
880
|
+
// Note that Date.now() is in milliseconds, and expirationDate in seconds.
|
|
881
|
+
const expired = storedClientSecret !== undefined &&
|
|
882
|
+
Math.floor(Date.now() / 1000) > expirationDate;
|
|
883
|
+
if (storedClientId && solidClientAuthnCore.isKnownClientType(storedClientType) && !expired) {
|
|
884
|
+
return storedClientSecret !== undefined
|
|
885
|
+
? {
|
|
886
|
+
clientId: storedClientId,
|
|
887
|
+
clientSecret: storedClientSecret,
|
|
888
|
+
clientName: storedClientName,
|
|
889
|
+
// Note: static clients are not applicable in a browser context.
|
|
890
|
+
clientType: "dynamic",
|
|
891
|
+
expiresAt: expirationDate,
|
|
892
|
+
}
|
|
893
|
+
: {
|
|
894
|
+
clientId: storedClientId,
|
|
895
|
+
clientName: storedClientName,
|
|
896
|
+
// Note: static clients are not applicable in a browser context.
|
|
897
|
+
clientType: storedClientType,
|
|
898
|
+
// The type assertion is required even though the type should match the declaration.
|
|
899
|
+
};
|
|
876
900
|
}
|
|
877
901
|
try {
|
|
878
902
|
const registeredClient = await oidcClientExt.registerClient(options, issuerConfig);
|
|
@@ -881,8 +905,9 @@ class ClientRegistrar {
|
|
|
881
905
|
clientId: registeredClient.clientId,
|
|
882
906
|
clientType: "dynamic",
|
|
883
907
|
};
|
|
884
|
-
if (registeredClient.clientSecret) {
|
|
908
|
+
if (registeredClient.clientSecret !== undefined) {
|
|
885
909
|
infoToSave.clientSecret = registeredClient.clientSecret;
|
|
910
|
+
infoToSave.expiresAt = String(registeredClient.expiresAt);
|
|
886
911
|
}
|
|
887
912
|
if (registeredClient.idTokenSignedResponseAlg) {
|
|
888
913
|
infoToSave.idTokenSignedResponseAlg =
|
|
@@ -897,7 +922,7 @@ class ClientRegistrar {
|
|
|
897
922
|
return registeredClient;
|
|
898
923
|
}
|
|
899
924
|
catch (error) {
|
|
900
|
-
throw new Error(`Client registration failed:
|
|
925
|
+
throw new Error(`Client registration failed.`, { cause: error });
|
|
901
926
|
}
|
|
902
927
|
}
|
|
903
928
|
}
|
|
@@ -976,6 +1001,9 @@ class ErrorOidcHandler {
|
|
|
976
1001
|
* @hidden
|
|
977
1002
|
*/
|
|
978
1003
|
class TokenRefresher {
|
|
1004
|
+
storageUtility;
|
|
1005
|
+
issuerConfigFetcher;
|
|
1006
|
+
clientRegistrar;
|
|
979
1007
|
constructor(storageUtility, issuerConfigFetcher, clientRegistrar) {
|
|
980
1008
|
this.storageUtility = storageUtility;
|
|
981
1009
|
this.issuerConfigFetcher = issuerConfigFetcher;
|
|
@@ -997,7 +1025,7 @@ class TokenRefresher {
|
|
|
997
1025
|
}
|
|
998
1026
|
const tokenSet = await oidcClientExt.refresh(refreshToken, oidcContext.issuerConfig, clientInfo, dpopKey);
|
|
999
1027
|
if (tokenSet.refreshToken !== undefined) {
|
|
1000
|
-
eventEmitter
|
|
1028
|
+
eventEmitter?.emit(solidClientAuthnCore.EVENTS.NEW_REFRESH_TOKEN, tokenSet.refreshToken);
|
|
1001
1029
|
}
|
|
1002
1030
|
return tokenSet;
|
|
1003
1031
|
}
|
|
@@ -1093,7 +1121,6 @@ const KEY_CURRENT_URL = `${solidClientAuthnCore.SOLID_CLIENT_AUTHN_KEY_PREFIX}cu
|
|
|
1093
1121
|
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
1094
1122
|
//
|
|
1095
1123
|
async function silentlyAuthenticate(sessionId, clientAuthn, session) {
|
|
1096
|
-
var _a;
|
|
1097
1124
|
const storedSessionInfo = await clientAuthn.validateCurrentSession(sessionId);
|
|
1098
1125
|
if (storedSessionInfo !== null) {
|
|
1099
1126
|
// It can be really useful to save the user's current browser location,
|
|
@@ -1108,19 +1135,31 @@ async function silentlyAuthenticate(sessionId, clientAuthn, session) {
|
|
|
1108
1135
|
redirectUrl: storedSessionInfo.redirectUrl,
|
|
1109
1136
|
clientId: storedSessionInfo.clientAppId,
|
|
1110
1137
|
clientSecret: storedSessionInfo.clientAppSecret,
|
|
1111
|
-
tokenType:
|
|
1138
|
+
tokenType: storedSessionInfo.tokenType ?? "DPoP",
|
|
1112
1139
|
}, session.events);
|
|
1113
1140
|
return true;
|
|
1114
1141
|
}
|
|
1115
1142
|
return false;
|
|
1116
1143
|
}
|
|
1117
1144
|
function isLoggedIn(sessionInfo) {
|
|
1118
|
-
return !!
|
|
1145
|
+
return !!sessionInfo?.isLoggedIn;
|
|
1119
1146
|
}
|
|
1120
1147
|
/**
|
|
1121
1148
|
* A {@link Session} object represents a user's session on an application. The session holds state, as it stores information enabling access to private resources after login for instance.
|
|
1122
1149
|
*/
|
|
1123
1150
|
class Session {
|
|
1151
|
+
/**
|
|
1152
|
+
* Information regarding the current session.
|
|
1153
|
+
*/
|
|
1154
|
+
info;
|
|
1155
|
+
/**
|
|
1156
|
+
* Session attribute exposing the EventEmitter interface, to listen on session
|
|
1157
|
+
* events such as login, logout, etc.
|
|
1158
|
+
* @since 1.15.0
|
|
1159
|
+
*/
|
|
1160
|
+
events;
|
|
1161
|
+
clientAuthentication;
|
|
1162
|
+
tokenRequestInProgress = false;
|
|
1124
1163
|
/**
|
|
1125
1164
|
* Session object constructor. Typically called as follows:
|
|
1126
1165
|
*
|
|
@@ -1137,149 +1176,6 @@ class Session {
|
|
|
1137
1176
|
*
|
|
1138
1177
|
*/
|
|
1139
1178
|
constructor(sessionOptions = {}, sessionId = undefined) {
|
|
1140
|
-
this.tokenRequestInProgress = false;
|
|
1141
|
-
/**
|
|
1142
|
-
* Triggers the login process. Note that this method will redirect the user away from your app.
|
|
1143
|
-
*
|
|
1144
|
-
* @param options Parameter to customize the login behaviour. In particular, two options are mandatory: `options.oidcIssuer`, the user's identity provider, and `options.redirectUrl`, the URL to which the user will be redirected after logging in their identity provider.
|
|
1145
|
-
* @returns This method should redirect the user away from the app: it does not return anything. The login process is completed by {@linkcode handleIncomingRedirect}.
|
|
1146
|
-
*/
|
|
1147
|
-
// Define these functions as properties so that they don't get accidentally re-bound.
|
|
1148
|
-
// Isn't Javascript fun?
|
|
1149
|
-
this.login = async (options) => {
|
|
1150
|
-
var _a;
|
|
1151
|
-
await this.clientAuthentication.login({
|
|
1152
|
-
sessionId: this.info.sessionId,
|
|
1153
|
-
...options,
|
|
1154
|
-
// Defaults the token type to DPoP
|
|
1155
|
-
tokenType: (_a = options.tokenType) !== null && _a !== void 0 ? _a : "DPoP",
|
|
1156
|
-
}, this.events);
|
|
1157
|
-
// `login` redirects the user away from the app,
|
|
1158
|
-
// so unless it throws an error, there is no code that should run afterwards
|
|
1159
|
-
// (since there is no "after" in the lifetime of the script).
|
|
1160
|
-
// Hence, this Promise never resolves:
|
|
1161
|
-
return new Promise(() => { });
|
|
1162
|
-
};
|
|
1163
|
-
/**
|
|
1164
|
-
* Fetches data using available login information. If the user is not logged in, this will behave as a regular `fetch`. The signature of this method is identical to the [canonical `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
|
1165
|
-
*
|
|
1166
|
-
* @param url The URL from which data should be fetched.
|
|
1167
|
-
* @param init Optional parameters customizing the request, by specifying an HTTP method, headers, a body, etc. Follows the [WHATWG Fetch Standard](https://fetch.spec.whatwg.org/).
|
|
1168
|
-
*/
|
|
1169
|
-
this.fetch = (url, init) => this.clientAuthentication.fetch(url, init);
|
|
1170
|
-
/**
|
|
1171
|
-
* An internal logout function, to control whether or not the logout signal
|
|
1172
|
-
* should be sent, i.e. if the logout was user-initiated or is the result of
|
|
1173
|
-
* an external event.
|
|
1174
|
-
*
|
|
1175
|
-
* @hidden
|
|
1176
|
-
*/
|
|
1177
|
-
this.internalLogout = async (emitSignal, options) => {
|
|
1178
|
-
// Clearing this value means that silent refresh will no longer be attempted.
|
|
1179
|
-
// In particular, in the case of a silent authentication error it prevents
|
|
1180
|
-
// from getting stuck in an authentication retries loop.
|
|
1181
|
-
window.localStorage.removeItem(KEY_CURRENT_SESSION);
|
|
1182
|
-
await this.clientAuthentication.logout(this.info.sessionId, options);
|
|
1183
|
-
this.info.isLoggedIn = false;
|
|
1184
|
-
if (emitSignal) {
|
|
1185
|
-
this.events.emit(solidClientAuthnCore.EVENTS.LOGOUT);
|
|
1186
|
-
}
|
|
1187
|
-
};
|
|
1188
|
-
/**
|
|
1189
|
-
* Logs the user out of the application.
|
|
1190
|
-
*
|
|
1191
|
-
* There are 2 types of logout supported by this library,
|
|
1192
|
-
* `app` logout and `idp` logout.
|
|
1193
|
-
*
|
|
1194
|
-
* App logout will log the user out within the application
|
|
1195
|
-
* by clearing any session data from the browser. It does
|
|
1196
|
-
* not log the user out of their Solid identity provider,
|
|
1197
|
-
* and should not redirect the user away.
|
|
1198
|
-
* App logout can be performed as follows:
|
|
1199
|
-
* ```typescript
|
|
1200
|
-
* await session.logout({ logoutType: 'app' });
|
|
1201
|
-
* ```
|
|
1202
|
-
*
|
|
1203
|
-
* IDP logout will log the user out of their Solid identity provider,
|
|
1204
|
-
* and will redirect the user away from the application to do so. In order
|
|
1205
|
-
* for users to be redirected back to `postLogoutUrl` you MUST include the
|
|
1206
|
-
* `postLogoutUrl` value in the `post_logout_redirect_uris` field in the
|
|
1207
|
-
* [Client ID Document](https://docs.inrupt.com/ess/latest/security/authentication/#client-identifier-client-id).
|
|
1208
|
-
* IDP logout can be performed as follows:
|
|
1209
|
-
* ```typescript
|
|
1210
|
-
* await session.logout({
|
|
1211
|
-
* logoutType: 'idp',
|
|
1212
|
-
* // An optional URL to redirect to after logout has completed;
|
|
1213
|
-
* // this MUST match a logout URL listed in the Client ID Document
|
|
1214
|
-
* // of the application that is logged in.
|
|
1215
|
-
* // If the application is logged in with a Client ID that is not
|
|
1216
|
-
* // a URI dereferencing to a Client ID Document then users will
|
|
1217
|
-
* // not be redirected back to the `postLogoutUrl` after logout.
|
|
1218
|
-
* postLogoutUrl: 'https://example.com/logout',
|
|
1219
|
-
* // An optional value to be included in the query parameters
|
|
1220
|
-
* // when the IDP provider redirects the user to the postLogoutRedirectUrl.
|
|
1221
|
-
* state: "my-state"
|
|
1222
|
-
* });
|
|
1223
|
-
* ```
|
|
1224
|
-
*/
|
|
1225
|
-
this.logout = async (options) => this.internalLogout(true, options);
|
|
1226
|
-
/**
|
|
1227
|
-
* Completes the login process by processing the information provided by the
|
|
1228
|
-
* Solid identity provider through redirect.
|
|
1229
|
-
*
|
|
1230
|
-
* @param options See {@link IHandleIncomingRedirectOptions}.
|
|
1231
|
-
*/
|
|
1232
|
-
this.handleIncomingRedirect = async (inputOptions = {}) => {
|
|
1233
|
-
var _a;
|
|
1234
|
-
if (this.info.isLoggedIn) {
|
|
1235
|
-
return this.info;
|
|
1236
|
-
}
|
|
1237
|
-
if (this.tokenRequestInProgress) {
|
|
1238
|
-
return undefined;
|
|
1239
|
-
}
|
|
1240
|
-
const options = typeof inputOptions === "string" ? { url: inputOptions } : inputOptions;
|
|
1241
|
-
const url = (_a = options.url) !== null && _a !== void 0 ? _a : window.location.href;
|
|
1242
|
-
this.tokenRequestInProgress = true;
|
|
1243
|
-
const sessionInfo = await this.clientAuthentication.handleIncomingRedirect(url, this.events);
|
|
1244
|
-
if (isLoggedIn(sessionInfo)) {
|
|
1245
|
-
this.setSessionInfo(sessionInfo);
|
|
1246
|
-
const currentUrl = window.localStorage.getItem(KEY_CURRENT_URL);
|
|
1247
|
-
if (currentUrl === null) {
|
|
1248
|
-
// The login event can only be triggered **after** the user has been
|
|
1249
|
-
// redirected from the IdP with access and ID tokens.
|
|
1250
|
-
this.events.emit(solidClientAuthnCore.EVENTS.LOGIN);
|
|
1251
|
-
}
|
|
1252
|
-
else {
|
|
1253
|
-
// If an URL is stored in local storage, we are being logged in after a
|
|
1254
|
-
// silent authentication, so remove our currently stored URL location
|
|
1255
|
-
// to clean up our state now that we are completing the re-login process.
|
|
1256
|
-
window.localStorage.removeItem(KEY_CURRENT_URL);
|
|
1257
|
-
this.events.emit(solidClientAuthnCore.EVENTS.SESSION_RESTORED, currentUrl);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
else if (options.restorePreviousSession === true) {
|
|
1261
|
-
// Silent authentication happens after a refresh, which means there are no
|
|
1262
|
-
// OAuth params in the current location IRI. It can only succeed if a session
|
|
1263
|
-
// was previously logged in, in which case its ID will be present with a known
|
|
1264
|
-
// identifier in local storage.
|
|
1265
|
-
// Check if we have a locally stored session ID...
|
|
1266
|
-
const storedSessionId = window.localStorage.getItem(KEY_CURRENT_SESSION);
|
|
1267
|
-
// ...if not, then there is no ID token, and so silent authentication cannot happen, but
|
|
1268
|
-
// if we do have a stored session ID, attempt to re-authenticate now silently.
|
|
1269
|
-
if (storedSessionId !== null) {
|
|
1270
|
-
const attemptedSilentAuthentication = await silentlyAuthenticate(storedSessionId, this.clientAuthentication, this);
|
|
1271
|
-
// At this point, we know that the main window will imminently be redirected.
|
|
1272
|
-
// However, this redirect is asynchronous and there is no way to halt execution
|
|
1273
|
-
// until it happens precisely. That's why the current Promise simply does not
|
|
1274
|
-
// resolve.
|
|
1275
|
-
if (attemptedSilentAuthentication) {
|
|
1276
|
-
return new Promise(() => { });
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
}
|
|
1280
|
-
this.tokenRequestInProgress = false;
|
|
1281
|
-
return sessionInfo;
|
|
1282
|
-
};
|
|
1283
1179
|
this.events = new EventEmitter();
|
|
1284
1180
|
if (sessionOptions.clientAuthentication) {
|
|
1285
1181
|
this.clientAuthentication = sessionOptions.clientAuthentication;
|
|
@@ -1303,7 +1199,7 @@ class Session {
|
|
|
1303
1199
|
}
|
|
1304
1200
|
else {
|
|
1305
1201
|
this.info = {
|
|
1306
|
-
sessionId: sessionId
|
|
1202
|
+
sessionId: sessionId ?? uuid.v4(),
|
|
1307
1203
|
isLoggedIn: false,
|
|
1308
1204
|
};
|
|
1309
1205
|
}
|
|
@@ -1315,6 +1211,146 @@ class Session {
|
|
|
1315
1211
|
this.events.on(solidClientAuthnCore.EVENTS.SESSION_EXPIRED, () => this.internalLogout(false));
|
|
1316
1212
|
this.events.on(solidClientAuthnCore.EVENTS.ERROR, () => this.internalLogout(false));
|
|
1317
1213
|
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Triggers the login process. Note that this method will redirect the user away from your app.
|
|
1216
|
+
*
|
|
1217
|
+
* @param options Parameter to customize the login behaviour. In particular, two options are mandatory: `options.oidcIssuer`, the user's identity provider, and `options.redirectUrl`, the URL to which the user will be redirected after logging in their identity provider.
|
|
1218
|
+
* @returns This method should redirect the user away from the app: it does not return anything. The login process is completed by {@linkcode handleIncomingRedirect}.
|
|
1219
|
+
*/
|
|
1220
|
+
// Define these functions as properties so that they don't get accidentally re-bound.
|
|
1221
|
+
// Isn't Javascript fun?
|
|
1222
|
+
login = async (options) => {
|
|
1223
|
+
await this.clientAuthentication.login({
|
|
1224
|
+
sessionId: this.info.sessionId,
|
|
1225
|
+
...options,
|
|
1226
|
+
// Defaults the token type to DPoP
|
|
1227
|
+
tokenType: options.tokenType ?? "DPoP",
|
|
1228
|
+
}, this.events);
|
|
1229
|
+
// `login` redirects the user away from the app,
|
|
1230
|
+
// so unless it throws an error, there is no code that should run afterwards
|
|
1231
|
+
// (since there is no "after" in the lifetime of the script).
|
|
1232
|
+
// Hence, this Promise never resolves:
|
|
1233
|
+
return new Promise(() => { });
|
|
1234
|
+
};
|
|
1235
|
+
/**
|
|
1236
|
+
* Fetches data using available login information. If the user is not logged in, this will behave as a regular `fetch`. The signature of this method is identical to the [canonical `fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
|
|
1237
|
+
*
|
|
1238
|
+
* @param url The URL from which data should be fetched.
|
|
1239
|
+
* @param init Optional parameters customizing the request, by specifying an HTTP method, headers, a body, etc. Follows the [WHATWG Fetch Standard](https://fetch.spec.whatwg.org/).
|
|
1240
|
+
*/
|
|
1241
|
+
fetch = (url, init) => this.clientAuthentication.fetch(url, init);
|
|
1242
|
+
/**
|
|
1243
|
+
* An internal logout function, to control whether or not the logout signal
|
|
1244
|
+
* should be sent, i.e. if the logout was user-initiated or is the result of
|
|
1245
|
+
* an external event.
|
|
1246
|
+
*
|
|
1247
|
+
* @hidden
|
|
1248
|
+
*/
|
|
1249
|
+
internalLogout = async (emitSignal, options) => {
|
|
1250
|
+
// Clearing this value means that silent refresh will no longer be attempted.
|
|
1251
|
+
// In particular, in the case of a silent authentication error it prevents
|
|
1252
|
+
// from getting stuck in an authentication retries loop.
|
|
1253
|
+
window.localStorage.removeItem(KEY_CURRENT_SESSION);
|
|
1254
|
+
await this.clientAuthentication.logout(this.info.sessionId, options);
|
|
1255
|
+
this.info.isLoggedIn = false;
|
|
1256
|
+
if (emitSignal) {
|
|
1257
|
+
this.events.emit(solidClientAuthnCore.EVENTS.LOGOUT);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
/**
|
|
1261
|
+
* Logs the user out of the application.
|
|
1262
|
+
*
|
|
1263
|
+
* There are 2 types of logout supported by this library,
|
|
1264
|
+
* `app` logout and `idp` logout.
|
|
1265
|
+
*
|
|
1266
|
+
* App logout will log the user out within the application
|
|
1267
|
+
* by clearing any session data from the browser. It does
|
|
1268
|
+
* not log the user out of their Solid identity provider,
|
|
1269
|
+
* and should not redirect the user away.
|
|
1270
|
+
* App logout can be performed as follows:
|
|
1271
|
+
* ```typescript
|
|
1272
|
+
* await session.logout({ logoutType: 'app' });
|
|
1273
|
+
* ```
|
|
1274
|
+
*
|
|
1275
|
+
* IDP logout will log the user out of their Solid identity provider,
|
|
1276
|
+
* and will redirect the user away from the application to do so. In order
|
|
1277
|
+
* for users to be redirected back to `postLogoutUrl` you MUST include the
|
|
1278
|
+
* `postLogoutUrl` value in the `post_logout_redirect_uris` field in the
|
|
1279
|
+
* [Client ID Document](https://docs.inrupt.com/ess/latest/security/authentication/#client-identifier-client-id).
|
|
1280
|
+
* IDP logout can be performed as follows:
|
|
1281
|
+
* ```typescript
|
|
1282
|
+
* await session.logout({
|
|
1283
|
+
* logoutType: 'idp',
|
|
1284
|
+
* // An optional URL to redirect to after logout has completed;
|
|
1285
|
+
* // this MUST match a logout URL listed in the Client ID Document
|
|
1286
|
+
* // of the application that is logged in.
|
|
1287
|
+
* // If the application is logged in with a Client ID that is not
|
|
1288
|
+
* // a URI dereferencing to a Client ID Document then users will
|
|
1289
|
+
* // not be redirected back to the `postLogoutUrl` after logout.
|
|
1290
|
+
* postLogoutUrl: 'https://example.com/logout',
|
|
1291
|
+
* // An optional value to be included in the query parameters
|
|
1292
|
+
* // when the IDP provider redirects the user to the postLogoutRedirectUrl.
|
|
1293
|
+
* state: "my-state"
|
|
1294
|
+
* });
|
|
1295
|
+
* ```
|
|
1296
|
+
*/
|
|
1297
|
+
logout = async (options) => this.internalLogout(true, options);
|
|
1298
|
+
/**
|
|
1299
|
+
* Completes the login process by processing the information provided by the
|
|
1300
|
+
* Solid identity provider through redirect.
|
|
1301
|
+
*
|
|
1302
|
+
* @param options See {@link IHandleIncomingRedirectOptions}.
|
|
1303
|
+
*/
|
|
1304
|
+
handleIncomingRedirect = async (inputOptions = {}) => {
|
|
1305
|
+
if (this.info.isLoggedIn) {
|
|
1306
|
+
return this.info;
|
|
1307
|
+
}
|
|
1308
|
+
if (this.tokenRequestInProgress) {
|
|
1309
|
+
return undefined;
|
|
1310
|
+
}
|
|
1311
|
+
const options = typeof inputOptions === "string" ? { url: inputOptions } : inputOptions;
|
|
1312
|
+
const url = options.url ?? window.location.href;
|
|
1313
|
+
this.tokenRequestInProgress = true;
|
|
1314
|
+
const sessionInfo = await this.clientAuthentication.handleIncomingRedirect(url, this.events);
|
|
1315
|
+
if (isLoggedIn(sessionInfo)) {
|
|
1316
|
+
this.setSessionInfo(sessionInfo);
|
|
1317
|
+
const currentUrl = window.localStorage.getItem(KEY_CURRENT_URL);
|
|
1318
|
+
if (currentUrl === null) {
|
|
1319
|
+
// The login event can only be triggered **after** the user has been
|
|
1320
|
+
// redirected from the IdP with access and ID tokens.
|
|
1321
|
+
this.events.emit(solidClientAuthnCore.EVENTS.LOGIN);
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
// If an URL is stored in local storage, we are being logged in after a
|
|
1325
|
+
// silent authentication, so remove our currently stored URL location
|
|
1326
|
+
// to clean up our state now that we are completing the re-login process.
|
|
1327
|
+
window.localStorage.removeItem(KEY_CURRENT_URL);
|
|
1328
|
+
this.events.emit(solidClientAuthnCore.EVENTS.SESSION_RESTORED, currentUrl);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
else if (options.restorePreviousSession === true) {
|
|
1332
|
+
// Silent authentication happens after a refresh, which means there are no
|
|
1333
|
+
// OAuth params in the current location IRI. It can only succeed if a session
|
|
1334
|
+
// was previously logged in, in which case its ID will be present with a known
|
|
1335
|
+
// identifier in local storage.
|
|
1336
|
+
// Check if we have a locally stored session ID...
|
|
1337
|
+
const storedSessionId = window.localStorage.getItem(KEY_CURRENT_SESSION);
|
|
1338
|
+
// ...if not, then there is no ID token, and so silent authentication cannot happen, but
|
|
1339
|
+
// if we do have a stored session ID, attempt to re-authenticate now silently.
|
|
1340
|
+
if (storedSessionId !== null) {
|
|
1341
|
+
const attemptedSilentAuthentication = await silentlyAuthenticate(storedSessionId, this.clientAuthentication, this);
|
|
1342
|
+
// At this point, we know that the main window will imminently be redirected.
|
|
1343
|
+
// However, this redirect is asynchronous and there is no way to halt execution
|
|
1344
|
+
// until it happens precisely. That's why the current Promise simply does not
|
|
1345
|
+
// resolve.
|
|
1346
|
+
if (attemptedSilentAuthentication) {
|
|
1347
|
+
return new Promise(() => { });
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
this.tokenRequestInProgress = false;
|
|
1352
|
+
return sessionInfo;
|
|
1353
|
+
};
|
|
1318
1354
|
setSessionInfo(sessionInfo) {
|
|
1319
1355
|
this.info.isLoggedIn = sessionInfo.isLoggedIn;
|
|
1320
1356
|
this.info.webId = sessionInfo.webId;
|