1 | /*
|
2 | * Copyright 2020 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 | */
|
25 | import { EventEmitter } from "events";
|
26 | import {
|
27 | ILoginInputOptions,
|
28 | ISessionInfo,
|
29 | IStorage,
|
30 | } from "@inrupt/solid-client-authn-core";
|
31 | import { v4 } from "uuid";
|
32 | import ClientAuthentication from "./ClientAuthentication";
|
33 | import { getClientAuthenticationWithDependencies } from "./dependencies";
|
34 |
|
35 | export interface ISessionOptions {
|
36 | /**
|
37 | * A private storage, unreachable to other scripts on the page. Typically in-memory.
|
38 | */
|
39 | secureStorage: IStorage;
|
40 | /**
|
41 | * A storage where non-sensitive information may be stored, potentially longer-lived than the secure storage.
|
42 | */
|
43 | insecureStorage: IStorage;
|
44 | /**
|
45 | * Details about the current session
|
46 | */
|
47 | sessionInfo: ISessionInfo;
|
48 | /**
|
49 | * An instance of the library core. Typically obtained using `getClientAuthenticationWithDependencies`.
|
50 | */
|
51 | clientAuthentication: ClientAuthentication;
|
52 | }
|
53 |
|
54 | /**
|
55 | * 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.
|
56 | */
|
57 | export class Session extends EventEmitter {
|
58 | /**
|
59 | * Information regarding the current session.
|
60 | */
|
61 | public readonly info: ISessionInfo;
|
62 |
|
63 | private clientAuthentication: ClientAuthentication;
|
64 |
|
65 | private tokenRequestInProgress = false;
|
66 |
|
67 | /**
|
68 | * Session object constructor. Typically called as follows:
|
69 | *
|
70 | * ```typescript
|
71 | * const session = new Session(
|
72 | * {
|
73 | * clientAuthentication: getClientAuthenticationWithDependencies({})
|
74 | * },
|
75 | * "mySession"
|
76 | * );
|
77 | * ```
|
78 | * @param sessionOptions The options enabling the correct instantiation of
|
79 | * the session. Either both storages or clientAuthentication are required. For
|
80 | * more information, see {@link ISessionOptions}.
|
81 | * @param sessionId A magic string uniquely identifying the session.
|
82 | *
|
83 | */
|
84 | constructor(
|
85 | sessionOptions: Partial<ISessionOptions> = {},
|
86 | sessionId?: string
|
87 | ) {
|
88 | super();
|
89 |
|
90 | if (sessionOptions.clientAuthentication) {
|
91 | this.clientAuthentication = sessionOptions.clientAuthentication;
|
92 | } else if (sessionOptions.secureStorage && sessionOptions.insecureStorage) {
|
93 | this.clientAuthentication = getClientAuthenticationWithDependencies({
|
94 | secureStorage: sessionOptions.secureStorage,
|
95 | insecureStorage: sessionOptions.insecureStorage,
|
96 | });
|
97 | } else {
|
98 | this.clientAuthentication = getClientAuthenticationWithDependencies({});
|
99 | }
|
100 |
|
101 | if (sessionOptions.sessionInfo) {
|
102 | this.info = {
|
103 | sessionId: sessionOptions.sessionInfo.sessionId,
|
104 | isLoggedIn: false,
|
105 | webId: sessionOptions.sessionInfo.webId,
|
106 | };
|
107 | } else {
|
108 | this.info = {
|
109 | sessionId: sessionId ?? v4(),
|
110 | isLoggedIn: false,
|
111 | };
|
112 | }
|
113 | }
|
114 |
|
115 | /**
|
116 | * Triggers the login process. Note that this method will redirect the user away from your app.
|
117 | *
|
118 | * @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.
|
119 | * @returns This method should redirect the user away from the app: it does not return anything. The login process is completed by {@linkcode handleIncomingRedirect}.
|
120 | */
|
121 | // Define these functions as properties so that they don't get accidentally re-bound.
|
122 | // Isn't Javascript fun?
|
123 | login = async (options: ILoginInputOptions): Promise<void> => {
|
124 | await this.clientAuthentication.login(this.info.sessionId, {
|
125 | ...options,
|
126 | });
|
127 | };
|
128 |
|
129 | /**
|
130 | * 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).
|
131 | *
|
132 | * @param url The URL from which data should be fetched.
|
133 | * @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/).
|
134 | */
|
135 | fetch = async (url: RequestInfo, init?: RequestInit): Promise<Response> => {
|
136 | if (!this.info.isLoggedIn) {
|
137 | // TODO: why does this.clientAuthentication.fetch return throws
|
138 | // ""'fetch' called on an object that does not implement interface Window"
|
139 | // when unauthenticated ?
|
140 | return window.fetch(url, init);
|
141 | }
|
142 | return this.clientAuthentication.fetch(url, init);
|
143 | };
|
144 |
|
145 | /**
|
146 | * Logs the user out of the application. This does not log the user out of the identity provider, and should not redirect the user away.
|
147 | */
|
148 | logout = async (): Promise<void> => {
|
149 | await this.clientAuthentication.logout(this.info.sessionId);
|
150 | this.emit("logout");
|
151 | };
|
152 |
|
153 | /**
|
154 | * Completes the login process by processing the information provided by the identity provider through redirect.
|
155 | *
|
156 | * @param url The URL of the page handling the redirect, including the query parameters — these contain the information to process the login.
|
157 | */
|
158 | handleIncomingRedirect = async (
|
159 | url: string
|
160 | ): Promise<ISessionInfo | undefined> => {
|
161 | if (this.info.isLoggedIn) {
|
162 | return this.info;
|
163 | }
|
164 | if (this.tokenRequestInProgress) {
|
165 | return undefined;
|
166 | }
|
167 | this.tokenRequestInProgress = true;
|
168 | const sessionInfo = await this.clientAuthentication.handleIncomingRedirect(
|
169 | url
|
170 | );
|
171 | if (sessionInfo) {
|
172 | if (sessionInfo.isLoggedIn) {
|
173 | // The login event can only be triggered **after** the user has been
|
174 | // redirected from the IdP with access and ID tokens.
|
175 | this.emit("login");
|
176 | }
|
177 | this.info.isLoggedIn = sessionInfo.isLoggedIn;
|
178 | this.info.webId = sessionInfo.webId;
|
179 | this.info.sessionId = sessionInfo.sessionId;
|
180 | }
|
181 | this.tokenRequestInProgress = false;
|
182 | return sessionInfo;
|
183 | };
|
184 |
|
185 | /**
|
186 | * Register a callback function to be called when a user completes login.
|
187 | *
|
188 | * The callback is called when {@link handleIncomingRedirect} completes successfully.
|
189 | *
|
190 | * @param callback The function called when a user completes login.
|
191 | */
|
192 | onLogin(callback: () => unknown): void {
|
193 | this.on("login", callback);
|
194 | }
|
195 |
|
196 | /**
|
197 | * Register a callback function to be called when a user logs out:
|
198 | *
|
199 | * @param callback The function called when a user completes logout.
|
200 | */
|
201 | onLogout(callback: () => unknown): void {
|
202 | this.on("logout", callback);
|
203 | }
|
204 | }
|