oidc-spa 6.4.0 → 6.5.0

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.
Files changed (39) hide show
  1. package/oidc/createOidc.d.ts +0 -2
  2. package/oidc/createOidc.js +307 -355
  3. package/oidc/createOidc.js.map +1 -1
  4. package/oidc/{createIsUserActive.js → isUserActive.js} +1 -1
  5. package/oidc/isUserActive.js.map +1 -0
  6. package/oidc/loginOrGoToAuthServer.d.ts +41 -0
  7. package/oidc/loginOrGoToAuthServer.js +296 -0
  8. package/oidc/loginOrGoToAuthServer.js.map +1 -0
  9. package/oidc/loginSilent.d.ts +2 -2
  10. package/oidc/loginSilent.js +2 -2
  11. package/oidc/loginSilent.js.map +1 -1
  12. package/oidc/oidcClientTsUserToTokens.d.ts +1 -0
  13. package/oidc/oidcClientTsUserToTokens.js +16 -0
  14. package/oidc/oidcClientTsUserToTokens.js.map +1 -1
  15. package/oidc/persistedAuthState.d.ts +9 -0
  16. package/oidc/persistedAuthState.js +28 -0
  17. package/oidc/persistedAuthState.js.map +1 -0
  18. package/package.json +26 -11
  19. package/src/oidc/createOidc.ts +291 -353
  20. package/src/oidc/loginOrGoToAuthServer.ts +267 -0
  21. package/src/oidc/loginSilent.ts +4 -4
  22. package/src/oidc/oidcClientTsUserToTokens.ts +24 -0
  23. package/src/oidc/persistedAuthState.ts +36 -0
  24. package/src/tools/ephemeralSessionStorage.ts +191 -0
  25. package/src/tools/haveSharedParentDomain.ts +13 -0
  26. package/tools/ephemeralSessionStorage.d.ts +3 -0
  27. package/tools/ephemeralSessionStorage.js +133 -0
  28. package/tools/ephemeralSessionStorage.js.map +1 -0
  29. package/tools/haveSharedParentDomain.d.ts +4 -0
  30. package/tools/haveSharedParentDomain.js +14 -0
  31. package/tools/haveSharedParentDomain.js.map +1 -0
  32. package/vendor/frontend/oidc-client-ts-and-jwt-decode.js +1 -1
  33. package/oidc/createIsUserActive.js.map +0 -1
  34. package/oidc/persistedLogoutState.d.ts +0 -9
  35. package/oidc/persistedLogoutState.js +0 -25
  36. package/oidc/persistedLogoutState.js.map +0 -1
  37. package/src/oidc/persistedLogoutState.ts +0 -29
  38. /package/oidc/{createIsUserActive.d.ts → isUserActive.d.ts} +0 -0
  39. /package/src/oidc/{createIsUserActive.ts → isUserActive.ts} +0 -0
@@ -0,0 +1,267 @@
1
+ import type { UserManager as OidcClientTsUserManager } from "../vendor/frontend/oidc-client-ts-and-jwt-decode";
2
+ import { toFullyQualifiedUrl } from "../tools/toFullyQualifiedUrl";
3
+ import { id, assert, type Equals } from "../vendor/frontend/tsafe";
4
+ import type { StateData } from "./StateData";
5
+
6
+ const GLOBAL_CONTEXT_KEY = "__oidc-spa.loginOrGoToAuthSever.globalContext";
7
+
8
+ declare global {
9
+ interface Window {
10
+ [GLOBAL_CONTEXT_KEY]: {
11
+ hasLoginBeenCalled: boolean;
12
+ URL_real: typeof URL;
13
+ };
14
+ }
15
+ }
16
+
17
+ window[GLOBAL_CONTEXT_KEY] ??= {
18
+ hasLoginBeenCalled: false,
19
+ URL_real: window.URL
20
+ };
21
+
22
+ const globalContext = window[GLOBAL_CONTEXT_KEY];
23
+
24
+ type Params = Params.Login | Params.GoToAuthServer;
25
+
26
+ namespace Params {
27
+ type Common = {
28
+ redirectUrl: string;
29
+ extraQueryParams_local: Record<string, string> | undefined;
30
+ transformUrlBeforeRedirect_local: ((url: string) => string) | undefined;
31
+ };
32
+
33
+ export type Login = Common & {
34
+ action: "login";
35
+ doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack: boolean;
36
+ doForceReloadOnBfCache: boolean;
37
+ doForceInteraction: boolean;
38
+ };
39
+
40
+ export type GoToAuthServer = Common & {
41
+ action: "go to auth server";
42
+ };
43
+ }
44
+
45
+ export function createLoginOrGoToAuthServer(params: {
46
+ configId: string;
47
+ oidcClientTsUserManager: OidcClientTsUserManager;
48
+ getExtraQueryParams: (() => Record<string, string>) | undefined;
49
+ transformUrlBeforeRedirect: ((url: string) => string) | undefined;
50
+ homeAndCallbackUrl: string;
51
+ log: typeof console.log | undefined;
52
+ }) {
53
+ const {
54
+ configId,
55
+ oidcClientTsUserManager,
56
+ getExtraQueryParams,
57
+ transformUrlBeforeRedirect,
58
+ homeAndCallbackUrl,
59
+ log
60
+ } = params;
61
+
62
+ const LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN = `oidc-spa.login-redirect-initiated:${configId}`;
63
+
64
+ let lastPublicUrl: string | undefined = undefined;
65
+
66
+ async function loginOrGoToAuthServer(params: Params): Promise<never> {
67
+ const {
68
+ redirectUrl: redirectUrl_params,
69
+ extraQueryParams_local,
70
+ transformUrlBeforeRedirect_local,
71
+ ...rest
72
+ } = params;
73
+
74
+ log?.("Calling loginOrGoToAuthServer", { params });
75
+
76
+ login_specific_handling: {
77
+ if (rest.action !== "login") {
78
+ break login_specific_handling;
79
+ }
80
+
81
+ if (globalContext.hasLoginBeenCalled) {
82
+ log?.("login() has already been called, ignoring the call");
83
+ return new Promise<never>(() => {});
84
+ }
85
+
86
+ globalContext.hasLoginBeenCalled = true;
87
+
88
+ bf_cache_handling: {
89
+ if (rest.doForceReloadOnBfCache) {
90
+ document.removeEventListener("visibilitychange", () => {
91
+ if (document.visibilityState === "visible") {
92
+ location.reload();
93
+ }
94
+ });
95
+ break bf_cache_handling;
96
+ }
97
+
98
+ localStorage.setItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN, "true");
99
+
100
+ const callback = () => {
101
+ if (document.visibilityState === "visible") {
102
+ document.removeEventListener("visibilitychange", callback);
103
+
104
+ log?.(
105
+ "We came back from the login pages and the state of the app has been restored"
106
+ );
107
+
108
+ if (rest.doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack) {
109
+ if (lastPublicUrl !== undefined) {
110
+ log?.(`Loading last public route: ${lastPublicUrl}`);
111
+ window.location.href = lastPublicUrl;
112
+ } else {
113
+ log?.("We don't know the last public route, navigating back in history");
114
+ window.history.back();
115
+ }
116
+ } else {
117
+ log?.("The current page doesn't require auth...");
118
+
119
+ if (
120
+ localStorage.getItem(
121
+ LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN
122
+ ) === null
123
+ ) {
124
+ log?.("but the user is now authenticated, reloading the page");
125
+ location.reload();
126
+ } else {
127
+ log?.(
128
+ "and the user doesn't seem to be authenticated, avoiding a reload"
129
+ );
130
+ globalContext.hasLoginBeenCalled = false;
131
+ }
132
+ }
133
+ }
134
+ };
135
+
136
+ log?.("Start listening to visibility change event");
137
+
138
+ document.addEventListener("visibilitychange", callback);
139
+ }
140
+ }
141
+
142
+ const redirectUrl = toFullyQualifiedUrl({
143
+ urlish: redirectUrl_params,
144
+ doAssertNoQueryParams: false
145
+ });
146
+
147
+ log?.(`redirectUrl: ${redirectUrl}`);
148
+
149
+ const transformUrl_oidcClientTs = (url: string) => {
150
+ (
151
+ [
152
+ [getExtraQueryParams?.(), transformUrlBeforeRedirect],
153
+ [extraQueryParams_local, transformUrlBeforeRedirect_local]
154
+ ] as const
155
+ ).forEach(([extraQueryParams, transformUrlBeforeRedirect]) => {
156
+ add_extra_query_params: {
157
+ if (extraQueryParams === undefined) {
158
+ break add_extra_query_params;
159
+ }
160
+
161
+ const url_obj = new URL(url);
162
+
163
+ for (const [name, value] of Object.entries(extraQueryParams)) {
164
+ url_obj.searchParams.set(name, value);
165
+ }
166
+
167
+ url = url_obj.href;
168
+ }
169
+
170
+ apply_transform_before_redirect: {
171
+ if (transformUrlBeforeRedirect === undefined) {
172
+ break apply_transform_before_redirect;
173
+ }
174
+ url = transformUrlBeforeRedirect(url);
175
+ }
176
+ });
177
+
178
+ return url;
179
+ };
180
+
181
+ const redirectMethod = (() => {
182
+ switch (rest.action) {
183
+ case "login":
184
+ return rest.doNavigateBackToLastPublicUrlIfTheTheUserNavigateBack
185
+ ? "replace"
186
+ : "assign";
187
+ case "go to auth server":
188
+ return "assign";
189
+ }
190
+ })();
191
+
192
+ log?.(`redirectMethod: ${redirectMethod}`);
193
+
194
+ const { extraQueryParams } = (() => {
195
+ const extraQueryParams: Record<string, string> = extraQueryParams_local ?? {};
196
+
197
+ read_query_params_added_by_transform_before_redirect: {
198
+ if (transformUrlBeforeRedirect_local === undefined) {
199
+ break read_query_params_added_by_transform_before_redirect;
200
+ }
201
+
202
+ let url_afterTransform;
203
+
204
+ try {
205
+ url_afterTransform = transformUrlBeforeRedirect_local("https://dummy.com");
206
+ } catch {
207
+ break read_query_params_added_by_transform_before_redirect;
208
+ }
209
+
210
+ for (const [name, value] of new URL(url_afterTransform).searchParams) {
211
+ extraQueryParams[name] = value;
212
+ }
213
+ }
214
+
215
+ return { extraQueryParams };
216
+ })();
217
+
218
+ await oidcClientTsUserManager.signinRedirect({
219
+ state: id<StateData>({
220
+ context: "redirect",
221
+ redirectUrl,
222
+ extraQueryParams,
223
+ hasBeenProcessedByCallback: false,
224
+ configId,
225
+ action: "login",
226
+ redirectUrl_consentRequiredCase: (() => {
227
+ switch (rest.action) {
228
+ case "login":
229
+ return lastPublicUrl ?? homeAndCallbackUrl;
230
+ case "go to auth server":
231
+ return redirectUrl;
232
+ }
233
+ })()
234
+ }),
235
+ redirectMethod,
236
+ prompt: (() => {
237
+ switch (rest.action) {
238
+ case "go to auth server":
239
+ return undefined;
240
+ case "login":
241
+ return rest.doForceInteraction ? "consent" : undefined;
242
+ }
243
+ assert<Equals<typeof rest, never>>;
244
+ })(),
245
+ transformUrl: transformUrl_oidcClientTs
246
+ });
247
+ return new Promise<never>(() => {});
248
+ }
249
+
250
+ function toCallBeforeReturningOidcLoggedIn() {
251
+ localStorage.removeItem(LOCAL_STORAGE_KEY_TO_CLEAR_WHEN_RETURNING_OIDC_LOGGED_IN);
252
+ }
253
+
254
+ function toCallBeforeReturningOidcNotLoggedIn() {
255
+ const realPushState = history.pushState.bind(history);
256
+ history.pushState = function pushState(...args) {
257
+ lastPublicUrl = window.location.href;
258
+ return realPushState(...args);
259
+ };
260
+ }
261
+
262
+ return {
263
+ loginOrGoToAuthServer,
264
+ toCallBeforeReturningOidcLoggedIn,
265
+ toCallBeforeReturningOidcNotLoggedIn
266
+ };
267
+ }
@@ -27,7 +27,7 @@ export function authResponseToUrl(authResponse: AuthResponse): string {
27
27
 
28
28
  type ResultOfLoginSilent =
29
29
  | {
30
- outcome: "success iframe";
30
+ outcome: "got auth response from iframe";
31
31
  authResponse: AuthResponse;
32
32
  }
33
33
  | {
@@ -35,7 +35,7 @@ type ResultOfLoginSilent =
35
35
  cause: "timeout" | "can't reach well-known oidc endpoint";
36
36
  }
37
37
  | {
38
- outcome: "refresh token used";
38
+ outcome: "token refreshed using refresh token";
39
39
  oidcClientTsUser: OidcClientTsUser;
40
40
  };
41
41
 
@@ -99,7 +99,7 @@ export async function loginSilent(params: {
99
99
  window.removeEventListener("message", listener);
100
100
 
101
101
  dResult.resolve({
102
- outcome: "success iframe",
102
+ outcome: "got auth response from iframe",
103
103
  authResponse
104
104
  });
105
105
  };
@@ -122,7 +122,7 @@ export async function loginSilent(params: {
122
122
  clearTimeout(timeout);
123
123
 
124
124
  dResult.resolve({
125
- outcome: "refresh token used",
125
+ outcome: "token refreshed using refresh token",
126
126
  oidcClientTsUser
127
127
  });
128
128
  },
@@ -117,3 +117,27 @@ export function oidcClientTsUserToTokens<DecodedIdToken extends Record<string, u
117
117
 
118
118
  return tokens;
119
119
  }
120
+
121
+ export function getMsBeforeExpiration(tokens: Oidc.Tokens): number {
122
+ // NOTE: In general the access token is supposed to have a shorter
123
+ // lifespan than the refresh token but we don't want to make any
124
+ // assumption here.
125
+ const tokenExpirationTime = Math.min(
126
+ tokens.accessTokenExpirationTime,
127
+ tokens.refreshTokenExpirationTime
128
+ );
129
+
130
+ const msBeforeExpiration = Math.min(
131
+ tokenExpirationTime - Date.now(),
132
+ // NOTE: We want to make sure we do not overflow the setTimeout
133
+ // that must be a 32 bit unsigned integer.
134
+ // This can happen if the tokenExpirationTime is more than 24.8 days in the future.
135
+ Math.pow(2, 31) - 1
136
+ );
137
+
138
+ if (msBeforeExpiration < 0) {
139
+ return 0;
140
+ }
141
+
142
+ return msBeforeExpiration;
143
+ }
@@ -0,0 +1,36 @@
1
+ import { assert } from "../vendor/frontend/tsafe";
2
+
3
+ function getKey(params: { configId: string }) {
4
+ const { configId } = params;
5
+
6
+ return `oidc-spa:auth-state:${configId}`;
7
+ }
8
+
9
+ type PersistedAuthState = "logged in" | "explicitly logged out";
10
+
11
+ export function persistAuthState(params: { configId: string; state: PersistedAuthState | undefined }) {
12
+ const { configId, state } = params;
13
+
14
+ const key = getKey({ configId });
15
+
16
+ if (state === undefined) {
17
+ localStorage.removeItem(key);
18
+ return;
19
+ }
20
+
21
+ localStorage.setItem(key, state);
22
+ }
23
+
24
+ export function getPersistedAuthState(params: { configId: string }): PersistedAuthState | undefined {
25
+ const { configId } = params;
26
+
27
+ const value = localStorage.getItem(getKey({ configId }));
28
+
29
+ if (value === null) {
30
+ return undefined;
31
+ }
32
+
33
+ assert(value === "logged in" || value === "explicitly logged out");
34
+
35
+ return value;
36
+ }
@@ -0,0 +1,191 @@
1
+ import { assert, typeGuard, id } from "../vendor/frontend/tsafe";
2
+
3
+ type SessionStorageItem_Parsed = {
4
+ __brand: typeof SessionStorageItem_Parsed.brand;
5
+ value: string;
6
+ expiresAtTime: number;
7
+ };
8
+
9
+ namespace SessionStorageItem_Parsed {
10
+ export const brand = "SessionStorageItem_Parsed";
11
+ }
12
+
13
+ function parseSessionStorageItem(
14
+ sessionStorageItemValue: string
15
+ ): SessionStorageItem_Parsed | undefined {
16
+ let json: unknown;
17
+
18
+ try {
19
+ json = JSON.parse(sessionStorageItemValue);
20
+ } catch {
21
+ return undefined;
22
+ }
23
+
24
+ if (
25
+ !typeGuard<SessionStorageItem_Parsed>(
26
+ json,
27
+ json instanceof Object &&
28
+ "__brand" in json &&
29
+ json.__brand === SessionStorageItem_Parsed.brand
30
+ )
31
+ ) {
32
+ return undefined;
33
+ }
34
+
35
+ return json;
36
+ }
37
+
38
+ type InMemoryItem = {
39
+ key: string;
40
+ value: string;
41
+ removeFromSessionStorage: (() => void) | undefined;
42
+ };
43
+
44
+ const SESSION_STORAGE_PREFIX = "ephemeral:";
45
+
46
+ function createStoreInSessionStorageAndScheduleRemovalInMemoryItem(params: {
47
+ key: string;
48
+ value: string;
49
+ remainingTtlMs: number;
50
+ }): InMemoryItem {
51
+ const { key, value, remainingTtlMs } = params;
52
+
53
+ const sessionStorageKey = `${SESSION_STORAGE_PREFIX}${key}`;
54
+
55
+ const removeFromSessionStorage = () => {
56
+ inMemoryItem.removeFromSessionStorage = undefined;
57
+ clearTimeout(timer);
58
+ sessionStorage.removeItem(sessionStorageKey);
59
+ };
60
+
61
+ const timer = setTimeout(() => removeFromSessionStorage(), remainingTtlMs);
62
+
63
+ const inMemoryItem: InMemoryItem = {
64
+ key,
65
+ value,
66
+ removeFromSessionStorage
67
+ };
68
+
69
+ sessionStorage.removeItem(sessionStorageKey);
70
+
71
+ sessionStorage.setItem(
72
+ sessionStorageKey,
73
+ JSON.stringify(
74
+ id<SessionStorageItem_Parsed>({
75
+ __brand: "SessionStorageItem_Parsed",
76
+ value,
77
+ expiresAtTime: Date.now() + remainingTtlMs
78
+ })
79
+ )
80
+ );
81
+
82
+ return inMemoryItem;
83
+ }
84
+
85
+ export function createEphemeralSessionStorage(params: { sessionStorageTtlMs: number }): Storage {
86
+ const { sessionStorageTtlMs } = params;
87
+
88
+ const inMemoryItems: InMemoryItem[] = [];
89
+
90
+ for (let i = 0; i < sessionStorage.length; i++) {
91
+ const sessionStorageKey = sessionStorage.key(i);
92
+ assert(sessionStorageKey !== null);
93
+
94
+ if (!sessionStorageKey.startsWith(SESSION_STORAGE_PREFIX)) {
95
+ continue;
96
+ }
97
+
98
+ const sessionStorageItem = sessionStorage.getItem(sessionStorageKey);
99
+
100
+ assert(sessionStorageItem !== null);
101
+
102
+ const sessionStorageItem_parsed = parseSessionStorageItem(sessionStorageItem);
103
+
104
+ if (sessionStorageItem_parsed === undefined) {
105
+ continue;
106
+ }
107
+
108
+ const remainingTtlMs = sessionStorageItem_parsed.expiresAtTime - Date.now();
109
+
110
+ if (remainingTtlMs <= 0) {
111
+ sessionStorage.removeItem(sessionStorageKey);
112
+ continue;
113
+ }
114
+
115
+ inMemoryItems.push(
116
+ createStoreInSessionStorageAndScheduleRemovalInMemoryItem({
117
+ key: sessionStorageKey.slice(SESSION_STORAGE_PREFIX.length),
118
+ value: sessionStorageItem_parsed.value,
119
+ remainingTtlMs
120
+ })
121
+ );
122
+ }
123
+
124
+ const storage = {
125
+ get length() {
126
+ return inMemoryItems.length;
127
+ },
128
+ key(index: number) {
129
+ const inMemoryItem = inMemoryItems[index];
130
+
131
+ if (inMemoryItem === undefined) {
132
+ return null;
133
+ }
134
+
135
+ return inMemoryItem.key;
136
+ },
137
+ removeItem(key: string) {
138
+ const inMemoryItem = inMemoryItems.find(item => item.key === key);
139
+
140
+ if (inMemoryItem === undefined) {
141
+ return;
142
+ }
143
+
144
+ inMemoryItem.removeFromSessionStorage?.();
145
+
146
+ const index = inMemoryItems.indexOf(inMemoryItem);
147
+
148
+ inMemoryItems.splice(index, 1);
149
+ },
150
+ clear() {
151
+ for (let i = 0; i < storage.length; i++) {
152
+ const key = storage.key(i);
153
+ assert(key !== null);
154
+ storage.removeItem(key);
155
+ }
156
+ },
157
+ getItem(key: string) {
158
+ const inMemoryItem = inMemoryItems.find(item => item.key === key);
159
+ if (inMemoryItem === undefined) {
160
+ return null;
161
+ }
162
+ return inMemoryItem.value;
163
+ },
164
+ setItem(key: string, value: string) {
165
+ let existingInMemoryItemIndex: number | undefined = undefined;
166
+
167
+ {
168
+ const inMemoryItem = inMemoryItems.find(item => item.key === key);
169
+
170
+ if (inMemoryItem !== undefined) {
171
+ inMemoryItem.removeFromSessionStorage?.();
172
+ existingInMemoryItemIndex = inMemoryItems.indexOf(inMemoryItem);
173
+ }
174
+ }
175
+
176
+ const inMemoryItem_new = createStoreInSessionStorageAndScheduleRemovalInMemoryItem({
177
+ key,
178
+ value,
179
+ remainingTtlMs: sessionStorageTtlMs
180
+ });
181
+
182
+ if (existingInMemoryItemIndex !== undefined) {
183
+ inMemoryItems[existingInMemoryItemIndex] = inMemoryItem_new;
184
+ } else {
185
+ inMemoryItems.push(inMemoryItem_new);
186
+ }
187
+ }
188
+ };
189
+
190
+ return storage;
191
+ }
@@ -0,0 +1,13 @@
1
+ export function getHaveSharedParentDomain(params: { url1: string; url2: string }): boolean {
2
+ const { url1, url2 } = params;
3
+
4
+ const url1Domain = new URL(url1).hostname;
5
+ const url2Domain = new URL(url2).hostname;
6
+
7
+ const getLevel2Domain = (url: string): string => {
8
+ const parts = url.split(".");
9
+ return parts.slice(parts.length - 2).join(".");
10
+ };
11
+
12
+ return getLevel2Domain(url1Domain) === getLevel2Domain(url2Domain);
13
+ }
@@ -0,0 +1,3 @@
1
+ export declare function createEphemeralSessionStorage(params: {
2
+ sessionStorageTtlMs: number;
3
+ }): Storage;