Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / containers / api / unAuthenticatedApi.ts
blob58df863ec2057f021c3f34d6ccb4661fea1d8a0d
1 import metrics from '@proton/metrics';
2 import {
3     PASSWORD_WRONG_ERROR,
4     auth,
5     auth2FA,
6     authMnemonic,
7     createSession,
8     payload,
9     revoke,
10     setCookies,
11     setLocalKey,
12     setRefreshCookies,
13 } from '@proton/shared/lib/api/auth';
14 import { getApiError, getIs401Error } from '@proton/shared/lib/api/helpers/apiErrorHelper';
15 import { createRefreshHandlers, getIsRefreshFailure, refresh } from '@proton/shared/lib/api/helpers/refreshHandlers';
16 import { createOnceHandler } from '@proton/shared/lib/apiHandlers';
17 import type { ChallengePayload } from '@proton/shared/lib/authentication/interface';
18 import { HTTP_ERROR_CODES } from '@proton/shared/lib/errors';
19 import { getUIDHeaderValue, withAuthHeaders, withUIDHeaders } from '@proton/shared/lib/fetch/headers';
20 import { getDateHeader } from '@proton/shared/lib/fetch/helpers';
21 import { createPromise, wait } from '@proton/shared/lib/helpers/promise';
22 import { setUID } from '@proton/shared/lib/helpers/sentry';
23 import { getItem, removeItem, setItem } from '@proton/shared/lib/helpers/sessionStorage';
24 import type { Api } from '@proton/shared/lib/interfaces';
25 import getRandomString from '@proton/utils/getRandomString';
26 import noop from '@proton/utils/noop';
28 const unAuthStorageKey = 'ua_uid';
30 const setupComplete = Symbol('setup complete');
31 const context: {
32     UID: string | undefined;
33     auth: { set: boolean; id: any; finalised: boolean };
34     api: Api;
35     refresh: () => void;
36     abortController: AbortController;
37     setup: null | typeof setupComplete | Promise<void>;
38     challenge: ReturnType<typeof createPromise<ChallengePayload | undefined>>;
39 } = {
40     UID: undefined,
41     api: undefined as any,
42     abortController: new AbortController(),
43     challenge: createPromise<ChallengePayload | undefined>(),
44     auth: { set: false, id: {}, finalised: false },
45     setup: null,
46     refresh: () => {},
49 export const updateUID = (UID: string) => {
50     setItem(unAuthStorageKey, UID);
52     setUID(UID);
53     metrics.setAuthHeaders(UID);
55     context.UID = UID;
56     context.auth.set = false;
57     context.auth.finalised = false;
58     context.auth.id = {};
59     context.abortController = new AbortController();
62 export const init = createOnceHandler(async () => {
63     context.abortController.abort();
65     const challengePromise = context.challenge.promise.catch(noop);
66     const challengePayload = await Promise.race([challengePromise, wait(300)]);
68     const response = await context.api<Response>({
69         ...createSession(challengePayload ? { Payload: challengePayload } : undefined),
70         silence: true,
71         headers: {
72             // This is here because it's required for clients that aren't in the min version
73             // And we won't put e.g. the standalone login for apps there
74             'x-enforce-unauthsession': true,
75         },
76         output: 'raw',
77     });
79     const { UID, AccessToken, RefreshToken } = await response.json();
80     await context.api({
81         ...withAuthHeaders(UID, AccessToken, setCookies({ UID, RefreshToken, State: getRandomString(24) })),
82         silence: true,
83     });
85     updateUID(UID);
87     if (!challengePayload) {
88         challengePromise
89             .then((challengePayload) => {
90                 if (!challengePayload) {
91                     return;
92                 }
93                 context
94                     .api({
95                         ...withUIDHeaders(UID, payload(challengePayload)),
96                         ignoreHandler: [HTTP_ERROR_CODES.UNAUTHORIZED],
97                         silence: true,
98                     })
99                     .catch(noop);
100             })
101             .catch(noop);
102     }
104     return response;
107 export const refreshHandler = createRefreshHandlers((UID: string) => {
108     return refresh(
109         () =>
110             context.api({
111                 ...withUIDHeaders(UID, setRefreshCookies()),
112                 ignoreHandler: [HTTP_ERROR_CODES.UNAUTHORIZED],
113                 output: 'raw',
114                 silence: 'true',
115             }),
116         1,
117         3
118     ).catch((e) => {
119         if (getIsRefreshFailure(e)) {
120             return init().then((result) => {
121                 return result;
122             });
123         }
124         throw e;
125     });
128 export const setup = async () => {
129     const oldUID = getItem(unAuthStorageKey);
130     if (oldUID) {
131         updateUID(oldUID);
132     } else {
133         return init();
134     }
137 const clearTabPersistedUID = () => {
138     removeItem(unAuthStorageKey);
141 const authConfig = auth({} as any, true);
142 const mnemonicAuthConfig = authMnemonic('', true);
143 const auth2FAConfig = auth2FA({ TwoFactorCode: '' });
144 const localKeyConfig = setLocalKey('');
146 const initSetup = (): Promise<void> | undefined => {
147     if (context.setup === setupComplete) {
148         return;
149     }
150     if (context.setup === null) {
151         context.setup = setup()
152             .then(() => {
153                 context.setup = setupComplete;
154             })
155             .catch(() => {
156                 context.setup = null;
157             });
158     }
159     return context.setup;
162 export const apiCallback: Api = async (config: any) => {
163     await initSetup();
164     const UID = context.UID;
165     if (!UID) {
166         return context.api(config);
167     }
169     // Note: requestUID !== UID means that this is an API request that is using an already established session, so we ignore unauth here.
170     const requestUID = getUIDHeaderValue(config.headers) ?? UID;
171     if (requestUID !== UID) {
172         return context.api(config);
173     }
175     // If an unauthenticated session attempts to signs in, the unauthenticated session has to be discarded so it's not
176     // accidentally re-used for another session. We do this before the response has returned to avoid race conditions,
177     // e.g. a user refreshing the page before the response has come back.
178     const isAuthUrl = [authConfig.url, mnemonicAuthConfig.url].includes(config.url);
179     if (isAuthUrl) {
180         clearTabPersistedUID();
181     }
183     const abortController = context.abortController;
184     const otherAbortCb = () => {
185         abortController.abort();
186     };
187     config.signal?.addEventListener('abort', otherAbortCb);
188     const id = {}; // Unique symbol for this run
190     try {
191         // This is set BEFORE the API calls finishes. This might give false positives (when the credentials are incorrect) but
192         // it'll ensure that the session is reset if a user hits the back button before the auth call finishes and credentials are correct.
193         // It's also reset in case the credentials are incorrect, but that assumes that one user can only trigger one auth process at a time.
194         if (isAuthUrl) {
195             context.auth.set = true;
196             context.auth.id = id;
197         }
199         if (config.url === localKeyConfig.url) {
200             context.auth.finalised = true;
201         }
203         const result = await context.api(
204             withUIDHeaders(UID, {
205                 ...config,
206                 signal: abortController.signal,
207                 ignoreHandler: [
208                     HTTP_ERROR_CODES.UNAUTHORIZED,
209                     ...(Array.isArray(config.ignoreHandler) ? config.ignoreHandler : []),
210                 ],
211                 silence:
212                     config.silence === true
213                         ? true
214                         : [HTTP_ERROR_CODES.UNAUTHORIZED, ...(Array.isArray(config.silence) ? config.silence : [])],
215             })
216         );
218         return result;
219     } catch (e: any) {
220         if (isAuthUrl && context.auth.id === id) {
221             context.auth.set = false;
222         }
223         if (config.url === localKeyConfig.url && context.auth.id === id) {
224             context.auth.finalised = false;
225         }
226         if (getIs401Error(e)) {
227             const { code } = getApiError(e);
228             // Don't attempt to refresh on 2fa 401 failures since the session has become invalidated.
229             // NOTE: Only one the PASSWORD_WRONG_ERROR code, since 401 is also triggered on session expiration.
230             if (config.url === auth2FAConfig.url && code === PASSWORD_WRONG_ERROR) {
231                 throw e;
232             }
233             return await refreshHandler(UID, getDateHeader(e?.response?.headers)).then(() => {
234                 return apiCallback(config);
235             });
236         }
237         throw e;
238     } finally {
239         config.signal?.removeEventListener('abort', otherAbortCb);
240     }
243 export const setApi = (api: Api) => {
244     context.api = api;
247 export const setChallenge = (data: ChallengePayload | undefined) => {
248     context.challenge.resolve(data);
251 export const startUnAuthFlow = createOnceHandler(async () => {
252     if (!(context.auth.set && context.UID)) {
253         return;
254     }
256     // Avoid deleting the session if it's been fully finalised and persisted
257     if (context.auth.set && context.UID && context.auth.finalised) {
258         updateUID('');
259         return init();
260     }
262     // Abort all previous request to prevent it triggering 401 and refresh
263     context.abortController.abort();
265     await context
266         .api(
267             withUIDHeaders(context.UID, {
268                 ...revoke(),
269                 silence: true,
270                 ignoreHandler: [HTTP_ERROR_CODES.UNAUTHORIZED],
271             })
272         )
273         .catch(noop);
275     await init();