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