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 | import { EventEmitter } from "events";
|
23 | import { ISessionInfo, IStorage } from "@inrupt/solid-client-authn-core";
|
24 | import { injectable } from "tsyringe";
|
25 | import { Session } from "./Session";
|
26 | import ClientAuthentication from "./ClientAuthentication";
|
27 | import { getClientAuthenticationWithDependencies } from "./dependencies";
|
28 |
|
29 | export interface ISessionManagerOptions {
|
30 | secureStorage?: IStorage;
|
31 | insecureStorage?: IStorage;
|
32 | }
|
33 |
|
34 | export interface ISessionManager {
|
35 | getSession(sessionId?: string): Promise<Session>;
|
36 | }
|
37 |
|
38 | /**
|
39 | * A SessionManager instance can be used to manage all the sessions in an
|
40 | * application, each session being associated with an individual user.
|
41 | */
|
42 | ()
|
43 | export class SessionManager extends EventEmitter implements ISessionManager {
|
44 | private clientAuthn: ClientAuthentication;
|
45 |
|
46 | private sessionRecords: Record<
|
47 | string,
|
48 | { session: Session; logoutCallback: () => unknown }
|
49 | > = {};
|
50 |
|
51 | private isInitialized = false;
|
52 |
|
53 | private handledIncomingRedirect = false;
|
54 |
|
55 | /**
|
56 | * Constructor for the SessionManager object. It is typically used as follows:
|
57 | *
|
58 | * ```typescript
|
59 | * import { SessionManager } from "@inrupt/solid-client-authn-browser";
|
60 | * import customStorage from "./myCustomStorage";
|
61 | *
|
62 | * const sessionManager = new SessionManager({
|
63 | * secureStorage: customStorage
|
64 | * });
|
65 | * ```
|
66 | * See {@link IStorage} for more information on how to define your own storage mechanism.
|
67 | *
|
68 | * @param options Options customizing the behaviour of the SessionManager, namely to store data appropriately.
|
69 | */
|
70 | constructor(options: ISessionManagerOptions = {}) {
|
71 | super();
|
72 | this.clientAuthn = getClientAuthenticationWithDependencies({
|
73 | secureStorage: options.secureStorage,
|
74 | insecureStorage: options.insecureStorage,
|
75 | });
|
76 | }
|
77 |
|
78 | private async init(): Promise<void> {
|
79 | if (!this.isInitialized) {
|
80 | await this.handleIncomingRedirect(window.location.href);
|
81 | this.isInitialized = true;
|
82 | }
|
83 | }
|
84 |
|
85 | private addNewSessionRecord(session: Session): Session {
|
86 | const logoutCallback = (): void => {
|
87 | this.emit("sessionLogout", session);
|
88 | };
|
89 | session.onLogout(logoutCallback);
|
90 | this.sessionRecords[session.info.sessionId] = {
|
91 | session,
|
92 | logoutCallback,
|
93 | };
|
94 | return session;
|
95 | }
|
96 |
|
97 | private getSessionFromCurrentSessionInfo(sessionInfo: ISessionInfo): Session {
|
98 | const sessionRecord = this.sessionRecords[sessionInfo.sessionId];
|
99 | if (sessionRecord) {
|
100 | sessionRecord.session.info.webId = sessionInfo.webId;
|
101 | sessionRecord.session.info.isLoggedIn = sessionInfo.isLoggedIn;
|
102 | return sessionRecord.session;
|
103 | }
|
104 | return this.addNewSessionRecord(
|
105 | new Session({
|
106 | clientAuthentication: this.clientAuthn,
|
107 | sessionInfo,
|
108 | })
|
109 | );
|
110 | }
|
111 |
|
112 | /**
|
113 | * @returns all the sessions currently managed by the session manager.
|
114 | */
|
115 | async getSessions(): Promise<Session[]> {
|
116 | await this.init();
|
117 | const sessionInfos = await this.clientAuthn.getAllSessionInfo();
|
118 | return sessionInfos.map((sessionInfo) =>
|
119 | this.getSessionFromCurrentSessionInfo(sessionInfo)
|
120 | );
|
121 | }
|
122 |
|
123 | /**
|
124 | * Creates a new session and adds it to the session manager.
|
125 | * If a session ID is not provided then a random UUID will be
|
126 | * assigned as the session ID. If the session of the provided
|
127 | * ID already exists then that session will be returned.
|
128 | *
|
129 | * @param sessionId An optional unique session identifier.
|
130 | * @returns A {@link Session} associated to the given ID.
|
131 | */
|
132 | async getSession(sessionId?: string): Promise<Session> {
|
133 | await this.init();
|
134 | let session: Session;
|
135 | if (sessionId) {
|
136 | const retrievedSessionInfo = await this.clientAuthn.getSessionInfo(
|
137 | sessionId
|
138 | );
|
139 | if (retrievedSessionInfo) {
|
140 | session = this.getSessionFromCurrentSessionInfo(retrievedSessionInfo);
|
141 | } else {
|
142 | session = this.addNewSessionRecord(
|
143 | new Session({ clientAuthentication: this.clientAuthn }, sessionId)
|
144 | );
|
145 | }
|
146 | } else {
|
147 | session = this.addNewSessionRecord(
|
148 | new Session({ clientAuthentication: this.clientAuthn })
|
149 | );
|
150 | }
|
151 | return session;
|
152 | }
|
153 |
|
154 | /**
|
155 | * @param sessionId A unique session identifier.
|
156 | * @returns A Promise resolving to true if a session associated to the given ID exists, and false if not.
|
157 | */
|
158 | async hasSession(sessionId: string): Promise<boolean> {
|
159 | await this.init();
|
160 | return (await this.clientAuthn.getSessionInfo(sessionId)) !== undefined;
|
161 | }
|
162 |
|
163 | /**
|
164 | * Registers a callback to be called when a session is logged in.
|
165 | *
|
166 | * @param callback a function executed when a session logs in, with the session as a parameter.
|
167 | */
|
168 | onSessionLogin(callback: (session: Session) => unknown): void {
|
169 | this.on("sessionLogin", callback);
|
170 | }
|
171 |
|
172 | /**
|
173 | * Registers a callback to be called when a session is logged out.
|
174 | *
|
175 | * @param callback a function executed when a session logs out, with the session as a parameter.
|
176 | */
|
177 | onSessionLogout(callback: (session: Session) => unknown): void {
|
178 | this.on("sessionLogout", callback);
|
179 | }
|
180 |
|
181 | /**
|
182 | * Removes a session from the pool managed by the manager. This is typically useful
|
183 | * when a user logs out of the application, so that the number of managed session
|
184 | * is not ever-growing. Note that this specific function **does not log out the session**,
|
185 | * it only removes references to it, so after this call the session will become unreachable.
|
186 | *
|
187 | * @param sessionId A unique session identifier.
|
188 | * @since 0.2.0
|
189 | */
|
190 | detachSession(sessionId: string): void {
|
191 | const sessionRecord = this.sessionRecords[sessionId];
|
192 | if (sessionRecord) {
|
193 | sessionRecord.session.removeListener(
|
194 | "onLogout",
|
195 | sessionRecord.logoutCallback
|
196 | );
|
197 | delete this.sessionRecords[sessionId];
|
198 | }
|
199 | }
|
200 |
|
201 | /**
|
202 | * Processes the information sent by the identity provider after
|
203 | * the user has logged in, in order to return a logged in {@link Session}.
|
204 | *
|
205 | * @param url The URL to which the user is being redirected.
|
206 | * @returns The {@link Session} that completed login if the process has been successful.
|
207 | */
|
208 | async handleIncomingRedirect(url: string): Promise<Session | undefined> {
|
209 | const sessionInfo = await this.clientAuthn.handleIncomingRedirect(url);
|
210 | if (sessionInfo) {
|
211 | const session = this.getSessionFromCurrentSessionInfo(sessionInfo);
|
212 | this.emit("sessionLogin", session);
|
213 | session.emit("login");
|
214 | return session;
|
215 | }
|
216 | return undefined;
|
217 | }
|
218 | }
|