1 import { c } from 'ttag';
3 import { serverTime, wasServerTimeEverUpdated } from '@proton/crypto';
4 import { auth2FA, getInfo } from '@proton/shared/lib/api/auth';
5 import { queryAvailableDomains } from '@proton/shared/lib/api/domains';
6 import { getApiErrorMessage } from '@proton/shared/lib/api/helpers/apiErrorHelper';
7 import type { ProductParam } from '@proton/shared/lib/apps/product';
8 import type { AuthResponse, AuthVersion, Fido2Data, InfoResponse } from '@proton/shared/lib/authentication/interface';
9 import loginWithFallback from '@proton/shared/lib/authentication/loginWithFallback';
10 import { persistSession } from '@proton/shared/lib/authentication/persistedSessionHelper';
11 import type { APP_NAMES } from '@proton/shared/lib/constants';
12 import { HTTP_ERROR_CODES } from '@proton/shared/lib/errors';
13 import { wait } from '@proton/shared/lib/helpers/promise';
14 import { captureMessage } from '@proton/shared/lib/helpers/sentry';
17 KeyTransparencyActivation,
18 VerifyOutboundPublicKeys,
21 } from '@proton/shared/lib/interfaces';
22 import { createKeyMigrationKTVerifier, createPreAuthKTVerifier } from '@proton/shared/lib/keyTransparency';
23 import { getRequiresPasswordSetup, getSentryError, migrateUser } from '@proton/shared/lib/keys';
24 import { handleSetupAddressKeys } from '@proton/shared/lib/keys/setupAddressKeys';
25 import { getHasV2KeysToUpgrade, upgradeV2KeysHelper } from '@proton/shared/lib/keys/upgradeKeysV2';
27 import type { ChallengeResult } from '../challenge/interface';
28 import { finalizeLogin } from './finalizeLogin';
29 import type { AuthActionResponse, AuthCacheResult, AuthSession } from './interface';
30 import { AuthStep, AuthType } from './interface';
31 import { getAuthTypes, getUnlockError, handleUnlockKey } from './loginHelper';
32 import { handlePrepareSSOData } from './ssoLoginHelper';
33 import { syncAddresses, syncSalts, syncUser } from './syncCache';
35 const handleKeyUpgrade = async ({
39 keyPassword: maybeKeyPassword,
42 cache: AuthCacheResult;
43 loginPassword: string;
44 clearKeyPassword: string;
46 isOnePasswordMode?: boolean;
48 const { api, preAuthKTVerifier, keyMigrationKTVerifier } = cache;
49 let keyPassword = maybeKeyPassword;
51 let [user, addresses] = await Promise.all([
52 cache.data.user || syncUser(cache),
53 cache.data.addresses || syncAddresses(cache),
56 const { preAuthKTVerify } = preAuthKTVerifier;
58 if (getHasV2KeysToUpgrade(user, addresses)) {
59 const newKeyPassword = await upgradeV2KeysHelper({
68 keyMigrationKTVerifier,
70 const error = getSentryError(e);
72 captureMessage('Key upgrade error', { extra: { error } });
76 if (newKeyPassword !== undefined) {
77 cache.data.user = undefined;
78 cache.data.addresses = undefined;
79 [user, addresses] = await Promise.all([syncUser(cache), syncAddresses(cache)]);
80 keyPassword = newKeyPassword;
84 const hasDoneMigration = await migrateUser({
90 keyMigrationKTVerifier,
92 const error = getSentryError(e);
94 captureMessage('Key migration error', {
95 extra: { error, serverTime: serverTime(), isServerTime: wasServerTimeEverUpdated() },
101 if (hasDoneMigration) {
102 cache.data.user = undefined;
103 cache.data.addresses = undefined;
106 return finalizeLogin({ cache, loginPassword, keyPassword, clearKeyPassword });
110 * Step 3. Handle unlock.
111 * Attempt to decrypt the primary private key with the password.
113 export const handleUnlock = async ({
118 cache: AuthCacheResult;
119 clearKeyPassword: string;
120 isOnePasswordMode: boolean;
123 data: { salts, user },
127 if (!salts || !user) {
128 throw new Error('Invalid state');
133 const unlockResult = await handleUnlockKey(user, salts, clearKeyPassword).catch(() => undefined);
135 throw getUnlockError();
138 return handleKeyUpgrade({
142 keyPassword: unlockResult.keyPassword,
147 export const handleReAuthKeyPassword = async ({
154 authSession: AuthSession;
156 clearKeyPassword: string;
160 const unlockResult = await handleUnlockKey(User, salts, clearKeyPassword).catch(() => undefined);
162 throw getUnlockError();
164 const newAuthSession = {
167 keyPassword: unlockResult.keyPassword,
169 const { clientKey, offlineKey } = await persistSession({
174 return { ...newAuthSession, clientKey, offlineKey, prompt: null };
177 export const handleSetupPassword = async ({ cache, newPassword }: { cache: AuthCacheResult; newPassword: string }) => {
178 const { api, username, preAuthKTVerifier } = cache;
180 const [domains, addresses] = await Promise.all([
181 api<{ Domains: string[] }>(queryAvailableDomains('signup')).then(({ Domains }) => Domains),
182 cache.data.addresses || (await syncAddresses(cache)),
185 const keyPassword = await handleSetupAddressKeys({
188 password: newPassword,
191 preAuthKTVerify: preAuthKTVerifier.preAuthKTVerify,
192 productParam: cache.productParam,
195 cache.data.user = undefined;
196 cache.data.addresses = undefined;
198 return finalizeLogin({
200 loginPassword: newPassword,
202 clearKeyPassword: newPassword,
206 const next = async ({ cache, from }: { cache: AuthCacheResult; from: AuthStep }): Promise<AuthActionResponse> => {
207 const { authType, authTypes, ignoreUnlock, authResponse, loginPassword } = cache;
209 if (from === AuthStep.LOGIN) {
210 if (authTypes.fido2 || authTypes.totp) {
218 // Special case for the admin panel (or sso login), return early since it can not get key salts or should setup keys.
220 return finalizeLogin({ cache, loginPassword });
223 const [user] = await Promise.all([cache.data.user || syncUser(cache), cache.data.salts || syncSalts(cache)]);
225 if (authType === AuthType.ExternalSSO) {
226 return handlePrepareSSOData({ cache });
229 if (user.Keys.length === 0) {
230 if (authResponse.TemporaryPassword) {
231 return { cache, to: AuthStep.NEW_PASSWORD };
233 if (getRequiresPasswordSetup(user, cache.setupVPN)) {
234 return handleSetupPassword({ cache, newPassword: loginPassword });
236 return finalizeLogin({ cache, loginPassword });
239 if (authTypes.unlock) {
246 return handleUnlock({ cache, clearKeyPassword: loginPassword, isOnePasswordMode: true });
249 export const handleFido2 = async ({
253 cache: AuthCacheResult;
255 }): Promise<AuthActionResponse> => {
256 const { api } = cache;
258 await api(auth2FA({ FIDO2: payload }));
260 return next({ cache, from: AuthStep.TWO_FA });
264 * Step 2. Handle TOTP.
265 * Unless there is another auth type active, the flow will continue until it's logged in.
267 export const handleTotp = async ({
271 cache: AuthCacheResult;
273 }): Promise<AuthActionResponse> => {
274 const { api } = cache;
276 await api(auth2FA({ TwoFactorCode: totp })).catch((e) => {
277 if (e.status === HTTP_ERROR_CODES.UNPROCESSABLE_ENTITY) {
278 const error: any = new Error(
279 getApiErrorMessage(e) || c('Error').t`Incorrect login credentials. Please try again.`
281 error.name = 'TOTPError';
288 return next({ cache, from: AuthStep.TWO_FA });
291 export const handleLogin = async ({
300 payload: ChallengeResult;
304 const infoResult = await api<InfoResponse>(getInfo({ username }));
305 const authResult = await loginWithFallback({
307 credentials: { username, password },
308 initialAuthInfo: infoResult,
312 return { infoResult, authResult };
315 export const handleNextLogin = async ({
329 verifyOutboundPublicKeys,
332 authVersion: AuthVersion;
333 authResponse: AuthResponse;
338 ignoreUnlock: boolean;
340 toApp: APP_NAMES | undefined;
342 ktActivation: KeyTransparencyActivation;
343 productParam: ProductParam;
344 verifyOutboundPublicKeys: VerifyOutboundPublicKeys | null;
345 }): Promise<AuthActionResponse> => {
346 const cache: AuthCacheResult = {
355 verifyOutboundPublicKeys,
356 authTypes: getAuthTypes(authResponse, appName),
359 loginPassword: password,
362 preAuthKTVerifier: createPreAuthKTVerifier(ktActivation),
363 keyMigrationKTVerifier: createKeyMigrationKTVerifier(ktActivation),
365 return next({ cache, from: AuthStep.LOGIN });