UNPKG

10.1 kBPlain TextView Raw
1/*
2 * Copyright 2021 Inrupt Inc.
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal in
6 * the Software without restriction, including without limitation the rights to use,
7 * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
8 * Software, and to permit persons to whom the Software is furnished to do so,
9 * subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
15 * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
16 * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
17 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
19 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 */
21
22/**
23 * @hidden
24 */
25import { EventEmitter } from "events";
26import {
27 ILoginInputOptions,
28 ISessionInfo,
29 IStorage,
30 ResourceServerSession,
31} from "@inrupt/solid-client-authn-core";
32import { v4 } from "uuid";
33import ClientAuthentication from "./ClientAuthentication";
34import { getClientAuthenticationWithDependencies } from "./dependencies";
35
36export interface ISessionOptions {
37 /**
38 * A private storage, unreachable to other scripts on the page. Typically in-memory.
39 */
40 secureStorage: IStorage;
41 /**
42 * A storage where non-sensitive information may be stored, potentially longer-lived than the secure storage.
43 */
44 insecureStorage: IStorage;
45 /**
46 * Details about the current session
47 */
48 sessionInfo: ISessionInfo;
49 /**
50 * An instance of the library core. Typically obtained using `getClientAuthenticationWithDependencies`.
51 */
52 clientAuthentication: ClientAuthentication;
53}
54
55/**
56 * A {@link Session} object represents a user's session on an application. The session holds state, as it stores information enabling acces to private resources after login for instance.
57 */
58export class Session extends EventEmitter {
59 /**
60 * Information regarding the current session.
61 */
62 public readonly info: ISessionInfo;
63
64 private clientAuthentication: ClientAuthentication;
65
66 private tokenRequestInProgress = false;
67
68 /**
69 * Session object constructor. Typically called as follows:
70 *
71 * ```typescript
72 * const session = new Session();
73 * ```
74 * @param sessionOptions The options enabling the correct instantiation of
75 * the session. Either both storages or clientAuthentication are required. For
76 * more information, see {@link ISessionOptions}.
77 * @param sessionId A magic string uniquely identifying the session.
78 *
79 */
80 constructor(
81 sessionOptions: Partial<ISessionOptions> = {},
82 sessionId?: string
83 ) {
84 super();
85
86 if (sessionOptions.clientAuthentication) {
87 this.clientAuthentication = sessionOptions.clientAuthentication;
88 } else if (sessionOptions.secureStorage && sessionOptions.insecureStorage) {
89 this.clientAuthentication = getClientAuthenticationWithDependencies({
90 secureStorage: sessionOptions.secureStorage,
91 insecureStorage: sessionOptions.insecureStorage,
92 });
93 } else {
94 this.clientAuthentication = getClientAuthenticationWithDependencies({});
95 }
96
97 if (sessionOptions.sessionInfo) {
98 this.info = {
99 sessionId: sessionOptions.sessionInfo.sessionId,
100 isLoggedIn: false,
101 webId: sessionOptions.sessionInfo.webId,
102 };
103 } else {
104 this.info = {
105 sessionId: sessionId ?? v4(),
106 isLoggedIn: false,
107 };
108 }
109 }
110
111 /**
112 * Triggers the login process. Note that this method will redirect the user away from your app.
113 *
114 * @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.
115 * @returns This method should redirect the user away from the app: it does not return anything. The login process is completed by {@linkcode handleIncomingRedirect}.
116 */
117 // Define these functions as properties so that they don't get accidentally re-bound.
118 // Isn't Javascript fun?
119 login = async (options: ILoginInputOptions): Promise<void> => {
120 await this.clientAuthentication.login(this.info.sessionId, {
121 ...options,
122 });
123 };
124
125 /**
126 * 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).
127 *
128 * @param url The URL from which data should be fetched.
129 * @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/).
130 */
131 fetch = async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
132 return this.clientAuthentication.fetch(url, init);
133 };
134
135 /**
136 * Logs the user out of the application. This does not log the user out of their Solid identity provider, and should not redirect the user away.
137 */
138 logout = async (): Promise<void> => {
139 await this.clientAuthentication.logout(this.info.sessionId);
140 this.info.isLoggedIn = false;
141 this.emit("logout");
142 };
143
144 /**
145 * Completes the login process by processing the information provided by the Solid identity provider through redirect.
146 *
147 * @param url The URL of the page handling the redirect, including the query parameters — these contain the information to process the login.
148 */
149 handleIncomingRedirect = async (
150 url: string
151 ): Promise<ISessionInfo | undefined> => {
152 if (this.info.isLoggedIn) {
153 return this.info;
154 }
155 if (this.tokenRequestInProgress) {
156 return undefined;
157 }
158
159 // Unfortunately, regular sessions are lost when the user refreshes the page or opens a new tab.
160 // While we're figuring out the API for a longer-term solution, as a temporary workaround some
161 // *resource* servers set a cookie that keeps the user logged in after authenticated requests,
162 // and expose the fact that they set it on a special endpoint.
163 // After login, we store that fact in LocalStorage. This means that we can now look for that
164 // data, and if present, indicate that the user is already logged in.
165 // Note that there are a lot of edge cases that won't work well with this approach, so it willl
166 // be removed in due time.
167 const storedSessionCookieReference = window.localStorage.getItem(
168 "tmp-resource-server-session-info"
169 );
170 if (typeof storedSessionCookieReference === "string") {
171 // TOOD: Re-use the type used when writing this data:
172 // https://github.com/inrupt/solid-client-authn-js/pull/920/files#diff-659ac87dfd3711f4cfcea3c7bf6970980f4740fd59df45f04c7977bffaa23e98R118
173 // To keep temporary code together
174 // eslint-disable-next-line no-inner-declarations
175 function isValidSessionCookieReference(
176 reference: Record<string, unknown>
177 ): reference is ResourceServerSession {
178 const resourceServers = Object.keys(
179 (reference as ResourceServerSession).sessions ?? {}
180 );
181 return (
182 typeof (reference as ResourceServerSession).webId === "string" &&
183 resourceServers.length > 0 &&
184 typeof (reference as ResourceServerSession).sessions[
185 resourceServers[0]
186 ].expiration === "number"
187 );
188 }
189 const reference = JSON.parse(storedSessionCookieReference);
190 if (isValidSessionCookieReference(reference)) {
191 const resourceServers = Object.keys(reference.sessions);
192 const webIdOrigin = new URL(reference.webId).hostname;
193 const ownResourceServer = resourceServers.find((resourceServer) => {
194 return new URL(resourceServer).hostname === webIdOrigin;
195 });
196 // Usually the user's WebID is also a Resource server for them,
197 // so we pick the expiration time for that. If it doesn't exist,
198 // we just pick the first (and probably only) one:
199 const relevantServer = ownResourceServer ?? resourceServers[0];
200 // If the cookie is valid for fewer than five minutes,
201 // pretend it's not valid anymore already, to avoid small misalignments
202 // resulting in invalid states:
203 if (
204 reference.sessions[relevantServer].expiration - Date.now() >
205 5 * 60 * 1000
206 ) {
207 this.info.isLoggedIn = true;
208 this.info.webId = reference.webId;
209 return this.info;
210 }
211 }
212 }
213 // end of temporary workaround.
214
215 this.tokenRequestInProgress = true;
216 const sessionInfo = await this.clientAuthentication.handleIncomingRedirect(
217 url
218 );
219 if (sessionInfo) {
220 this.info.isLoggedIn = sessionInfo.isLoggedIn;
221 this.info.webId = sessionInfo.webId;
222 this.info.sessionId = sessionInfo.sessionId;
223 if (sessionInfo.isLoggedIn) {
224 // The login event can only be triggered **after** the user has been
225 // redirected from the IdP with access and ID tokens.
226 this.emit("login");
227 }
228 }
229 this.tokenRequestInProgress = false;
230 return sessionInfo;
231 };
232
233 /**
234 * Register a callback function to be called when a user completes login.
235 *
236 * The callback is called when {@link handleIncomingRedirect} completes successfully.
237 *
238 * @param callback The function called when a user completes login.
239 */
240 onLogin(callback: () => unknown): void {
241 this.on("login", callback);
242 }
243
244 /**
245 * Register a callback function to be called when a user logs out:
246 *
247 * @param callback The function called when a user completes logout.
248 */
249 onLogout(callback: () => unknown): void {
250 this.on("logout", callback);
251 }
252}