1 import { ForkSearchParameters } from '@proton/shared/lib/authentication/fork';
2 import noop from '@proton/utils/noop';
4 import { removeLastRefreshDate } from '../api/helpers/refreshStorage';
5 import { getAppHref } from '../apps/helper';
6 import { getSlugFromApp } from '../apps/slugHelper';
7 import type { APP_NAMES } from '../constants';
8 import { APPS, SSO_PATHS } from '../constants';
9 import { replaceUrl } from '../helpers/browser';
10 import { decodeBase64URL, encodeBase64URL } from '../helpers/encoding';
11 import type { PersistedSession } from './SessionInterface';
12 import type { AuthenticationStore } from './createAuthenticationStore';
13 import { requestFork } from './fork/consume';
14 import type { ExtraSessionForkData } from './interface';
15 import { stripLocalBasenameFromPathname } from './pathnameHelper';
16 import { getPersistedSession, removePersistedSession } from './persistedSessionStorage';
18 const clearRecoveryParam = 'clear-recovery';
20 interface PassedSession {
25 export const serializeLogoutURL = ({
27 clearDeviceRecoveryData,
33 persistedSessions: PersistedSession[];
34 clearDeviceRecoveryData: boolean | undefined;
36 const slug = getSlugFromApp(appName);
37 if (slug && appName !== APPS.PROTONACCOUNT) {
38 url.searchParams.set('product', slug);
40 if (clearDeviceRecoveryData) {
41 url.searchParams.set(clearRecoveryParam, JSON.stringify(clearDeviceRecoveryData));
43 if (persistedSessions.length) {
44 const hashParams = new URLSearchParams();
45 const sessions = persistedSessions.map((persistedSession): PassedSession => {
47 id: persistedSession.UserID,
48 s: persistedSession.isSubUser,
51 hashParams.set('sessions', encodeBase64URL(JSON.stringify(sessions)));
52 url.hash = hashParams.toString();
57 export const getLogoutURL = ({
61 persistedSessions: inputPersistedSessions,
62 clearDeviceRecoveryData,
66 type?: 'full' | 'local';
68 mode: 'sso' | 'standalone';
69 persistedSessions: PersistedSession[];
70 clearDeviceRecoveryData?: boolean;
71 reason: 'signout' | 'session-expired';
75 // If it's not a full logout on account, we just strip the local id from the path in order to get redirected back
76 if (appName === APPS.PROTONACCOUNT && type !== 'full') {
77 const url = new URL(window.location.href);
78 url.pathname = stripLocalBasenameFromPathname(url.pathname);
79 if (localID !== undefined) {
80 url.searchParams.set(ForkSearchParameters.LocalID, `${localID}`);
82 return url.toString();
85 const url = new URL(getAppHref(SSO_PATHS.SWITCH, APPS.PROTONACCOUNT));
86 url.searchParams.set('reason', reason);
87 const persistedSessions = type === 'full' ? inputPersistedSessions : [];
88 return serializeLogoutURL({ appName, url, persistedSessions, clearDeviceRecoveryData }).toString();
91 return SSO_PATHS.LOGIN;
94 const parseSessions = (sessions: string | null) => {
96 const result = JSON.parse(decodeBase64URL(sessions || ''));
97 if (Array.isArray(result)) {
98 return result.map((session): PassedSession => {
101 s: session.s === true,
111 export const parseLogoutURL = (url: URL) => {
112 const searchParams = new URLSearchParams(url.search);
113 const hashParams = new URLSearchParams(url.hash.slice(1));
114 const sessions = parseSessions(hashParams.get('sessions'));
115 const reason = searchParams.get('reason') || searchParams.get('flow');
117 logout: reason === 'signout' || reason === 'logout',
118 clearDeviceRecoveryData: searchParams.get(clearRecoveryParam) === 'true',
123 export const handleLogout = async ({
127 clearDeviceRecoveryData,
132 type: 'full' | 'local';
133 authentication: AuthenticationStore;
134 clearDeviceRecoveryData?: boolean;
135 reason?: 'signout' | 'session-expired';
136 extra?: ExtraSessionForkData;
138 const UID = authentication.UID;
139 const localID = extra?.localID ?? authentication.localID;
140 const mode = authentication.mode;
142 const persistedSessions: PersistedSession[] = [];
145 removeLastRefreshDate(UID);
148 if (localID !== undefined && mode === 'sso') {
149 const persistedSession = getPersistedSession(localID);
150 if (persistedSession) {
151 persistedSessions.push(persistedSession);
152 await removePersistedSession(localID, UID).catch(noop);
156 authentication.logout();
158 if (appName === APPS.PROTONACCOUNT || appName === APPS.PROTONVPN_SETTINGS || mode !== 'sso' || type === 'full') {
166 clearDeviceRecoveryData,
171 requestFork({ fromApp: appName, localID, reason, extra });
175 export const handleInvalidSession = ({
181 authentication: AuthenticationStore;
182 extra?: ExtraSessionForkData;
184 // A session that is invalid should just do a local deletion on its own subdomain, to check if the session still exists on account.
187 reason: 'session-expired',
190 clearDeviceRecoveryData: false,