diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 8e0cf66498..4c7d621c14 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -10,6 +10,7 @@ import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { CipherResponse } from '../models/response/cipherResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { SyncResponse } from '../models/response/syncResponse'; export abstract class ApiService { @@ -20,7 +21,7 @@ export abstract class ApiService { logoutCallback: Function; setUrls: (urls: EnvironmentUrls) => void; - postIdentityToken: (request: TokenRequest) => Promise; + postIdentityToken: (request: TokenRequest) => Promise; refreshIdentityToken: () => Promise; postTwoFactorEmail: (request: TwoFactorEmailRequest) => Promise; getAccountRevisionDate: () => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index 45f7ca034d..08ca102729 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -1,5 +1,15 @@ +import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; + +import { AuthResult } from '../models/domain/authResult'; + export abstract class AuthService { - logIn: (email: string, masterPassword: string, twoFactorProvider?: number, twoFactorToken?: string, - remember?: boolean) => Promise; + email: string; + masterPasswordHash: string; + twoFactorProviders: Map; + + logIn: (email: string, masterPassword: string) => Promise; + logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, + remember?: boolean) => Promise; logOut: (callback: Function) => void; + getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; } diff --git a/src/abstractions/platformUtils.service.ts b/src/abstractions/platformUtils.service.ts index be8e3cb310..d0c16bf2b0 100644 --- a/src/abstractions/platformUtils.service.ts +++ b/src/abstractions/platformUtils.service.ts @@ -15,4 +15,5 @@ export abstract class PlatformUtilsService { launchUri: (uri: string, options?: any) => void; saveFile: (win: Window, blobData: any, blobOptions: any, fileName: string) => void; getApplicationVersion: () => string; + supportsU2f: (win: Window) => boolean; } diff --git a/src/enums/index.ts b/src/enums/index.ts index d1fff1d889..efdf363c89 100644 --- a/src/enums/index.ts +++ b/src/enums/index.ts @@ -3,3 +3,4 @@ export { DeviceType } from './deviceType'; export { EncryptionType } from './encryptionType'; export { FieldType } from './fieldType'; export { SecureNoteType } from './secureNoteType'; +export { TwoFactorProviderType } from './twoFactorProviderType'; diff --git a/src/enums/twoFactorProviderType.ts b/src/enums/twoFactorProviderType.ts new file mode 100644 index 0000000000..9d767ae026 --- /dev/null +++ b/src/enums/twoFactorProviderType.ts @@ -0,0 +1,8 @@ +export enum TwoFactorProviderType { + Authenticator = 0, + Email = 1, + Duo = 2, + Yubikey = 3, + U2f = 4, + Remember = 5, +} diff --git a/src/models/domain/authResult.ts b/src/models/domain/authResult.ts new file mode 100644 index 0000000000..cb4bb57c65 --- /dev/null +++ b/src/models/domain/authResult.ts @@ -0,0 +1,6 @@ +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +export class AuthResult { + twoFactor: boolean = false; + twoFactorProviders: Map = null; +} diff --git a/src/models/domain/index.ts b/src/models/domain/index.ts index 698585a3c9..7597a746d3 100644 --- a/src/models/domain/index.ts +++ b/src/models/domain/index.ts @@ -1,4 +1,5 @@ export { Attachment } from './attachment'; +export { AuthResult } from './authResult'; export { Card } from './card'; export { Cipher } from './cipher'; export { CipherString } from './cipherString'; diff --git a/src/models/response/identityTwoFactorResponse.ts b/src/models/response/identityTwoFactorResponse.ts new file mode 100644 index 0000000000..2adeeaa053 --- /dev/null +++ b/src/models/response/identityTwoFactorResponse.ts @@ -0,0 +1,17 @@ +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +export class IdentityTwoFactorResponse { + twoFactorProviders: TwoFactorProviderType[]; + twoFactorProviders2 = new Map(); + + constructor(response: any) { + this.twoFactorProviders = response.TwoFactorProviders; + if (response.TwoFactorProviders2 != null) { + for (const prop in response.TwoFactorProviders2) { + if (response.TwoFactorProviders2.hasOwnProperty(prop)) { + this.twoFactorProviders2.set(parseInt(prop, null), response.TwoFactorProviders2[prop]); + } + } + } + } +} diff --git a/src/models/response/index.ts b/src/models/response/index.ts index 00f85d119d..98829b10ea 100644 --- a/src/models/response/index.ts +++ b/src/models/response/index.ts @@ -7,6 +7,7 @@ export { ErrorResponse } from './errorResponse'; export { FolderResponse } from './folderResponse'; export { GlobalDomainResponse } from './globalDomainResponse'; export { IdentityTokenResponse } from './identityTokenResponse'; +export { IdentityTwoFactorResponse } from './identityTwoFactorResponse'; export { KeysResponse } from './keysResponse'; export { ListResponse } from './listResponse'; export { ProfileOrganizationResponse } from './profileOrganizationResponse'; diff --git a/src/services/api.service.ts b/src/services/api.service.ts index 5fd95daac4..1e452af755 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -17,6 +17,7 @@ import { CipherResponse } from '../models/response/cipherResponse'; import { ErrorResponse } from '../models/response/errorResponse'; import { FolderResponse } from '../models/response/folderResponse'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; import { SyncResponse } from '../models/response/syncResponse'; export class ApiService implements ApiServiceAbstraction { @@ -72,7 +73,7 @@ export class ApiService implements ApiServiceAbstraction { // Auth APIs - async postIdentityToken(request: TokenRequest): Promise { + async postIdentityToken(request: TokenRequest): Promise { const response = await fetch(new Request(this.identityBaseUrl + '/connect/token', { body: this.qsStringify(request.toIdentityToken()), cache: 'no-cache', @@ -96,7 +97,7 @@ export class ApiService implements ApiServiceAbstraction { } else if (response.status === 400 && responseJson.TwoFactorProviders2 && Object.keys(responseJson.TwoFactorProviders2).length) { await this.tokenService.clearTwoFactorToken(request.email); - return responseJson.TwoFactorProviders2; + return new IdentityTwoFactorResponse(responseJson); } } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 2d2328c6a3..c12904b1b5 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,40 +1,148 @@ +import { TwoFactorProviderType } from '../enums/twoFactorProviderType'; + +import { AuthResult } from '../models/domain/authResult'; +import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; + import { DeviceRequest } from '../models/request/deviceRequest'; import { TokenRequest } from '../models/request/tokenRequest'; +import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; +import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; + import { ConstantsService } from '../services/constants.service'; import { ApiService } from '../abstractions/api.service'; import { AppIdService } from '../abstractions/appId.service'; import { CryptoService } from '../abstractions/crypto.service'; +import { I18nService } from '../abstractions/i18n.service'; import { MessagingService } from '../abstractions/messaging.service'; import { PlatformUtilsService } from '../abstractions/platformUtils.service'; import { TokenService } from '../abstractions/token.service'; import { UserService } from '../abstractions/user.service'; +export const TwoFactorProviders = { + [TwoFactorProviderType.Authenticator]: { + name: null as string, + description: null as string, + active: true, + free: true, + displayOrder: 0, + priority: 1, + }, + [TwoFactorProviderType.Yubikey]: { + name: null as string, + description: null as string, + active: true, + free: false, + displayOrder: 1, + priority: 3, + }, + [TwoFactorProviderType.Duo]: { + name: 'Duo', + description: null as string, + active: true, + free: false, + displayOrder: 2, + priority: 2, + }, + [TwoFactorProviderType.U2f]: { + name: null as string, + description: null as string, + active: true, + free: false, + displayOrder: 3, + priority: 4, + }, + [TwoFactorProviderType.Email]: { + name: null as string, + description: null as string, + active: true, + free: false, + displayOrder: 4, + priority: 0, + }, +}; + export class AuthService { - constructor(public cryptoService: CryptoService, public apiService: ApiService, public userService: UserService, - public tokenService: TokenService, public appIdService: AppIdService, - public platformUtilsService: PlatformUtilsService, public constantsService: ConstantsService, - public messagingService: MessagingService) { + email: string; + masterPasswordHash: string; + twoFactorProviders: Map; + + private key: SymmetricCryptoKey; + + constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, + private tokenService: TokenService, private appIdService: AppIdService, private i18nService: I18nService, + private platformUtilsService: PlatformUtilsService, private constantsService: ConstantsService, + private messagingService: MessagingService) { } - async logIn(email: string, masterPassword: string, twoFactorProvider?: number, - twoFactorToken?: string, remember?: boolean) { + init() { + TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t('emailTitle'); + TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t('emailDesc'); + + TwoFactorProviders[TwoFactorProviderType.Authenticator].name = this.i18nService.t('authenticatorAppTitle'); + TwoFactorProviders[TwoFactorProviderType.Authenticator].description = + this.i18nService.t('authenticatorAppDesc'); + + TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t('duoDesc'); + + TwoFactorProviders[TwoFactorProviderType.U2f].name = this.i18nService.t('u2fTitle'); + TwoFactorProviders[TwoFactorProviderType.U2f].description = this.i18nService.t('u2fDesc'); + + TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t('yubiKeyTitle'); + TwoFactorProviders[TwoFactorProviderType.Yubikey].description = this.i18nService.t('yubiKeyDesc'); + } + + async logIn(email: string, masterPassword: string): Promise { email = email.toLowerCase(); - const key = this.cryptoService.makeKey(masterPassword, email); - const appId = await this.appIdService.getAppId(); - const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); + return await this.logInHelper(email, hashedPassword, key); + } + async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, + remember?: boolean): Promise { + return await this.logInHelper(this.email, this.masterPasswordHash, this.key, twoFactorProvider, + twoFactorToken, remember); + } + + logOut(callback: Function) { + callback(); + } + + getDefaultTwoFactorProvider(u2fSupported: boolean): TwoFactorProviderType { + if (this.twoFactorProviders == null) { + return null; + } + + let providerType: TwoFactorProviderType = null; + let providerPriority = -1; + this.twoFactorProviders.forEach((value, type) => { + const provider = (TwoFactorProviders as any)[type]; + if (provider != null && provider.active && provider.priority > providerPriority) { + if (type === TwoFactorProviderType.U2f && !u2fSupported) { + return; + } + + providerType = type; + providerPriority = provider.priority; + } + }); + + return providerType; + } + + private async logInHelper(email: string, hashedPassword: string, key: SymmetricCryptoKey, + twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { + const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); + const appId = await this.appIdService.getAppId(); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); let request: TokenRequest; - if (twoFactorToken != null && twoFactorProvider != null) { request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, deviceRequest); - } else if (storedTwoFactorToken) { + } else if (storedTwoFactorToken != null) { request = new TokenRequest(email, hashedPassword, this.constantsService.twoFactorProvider.remember, storedTwoFactorToken, false, deviceRequest); } else { @@ -42,37 +150,41 @@ export class AuthService { } const response = await this.apiService.postIdentityToken(request); - if (!response) { - return; - } - if (!response.accessToken) { + this.clearState(); + const result = new AuthResult(); + result.twoFactor = !(response as any).accessToken; + + if (result.twoFactor) { // two factor required - return { - twoFactor: true, - twoFactorProviders: response, - }; + const twoFactorResponse = response as IdentityTwoFactorResponse; + this.email = email; + this.masterPasswordHash = hashedPassword; + this.key = key; + this.twoFactorProviders = twoFactorResponse.twoFactorProviders2; + result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; + return result; } - if (response.twoFactorToken) { - this.tokenService.setTwoFactorToken(response.twoFactorToken, email); + const tokenResponse = response as IdentityTokenResponse; + if (tokenResponse.twoFactorToken != null) { + this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } - await this.tokenService.setTokens(response.accessToken, response.refreshToken); + await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); await this.cryptoService.setKey(key); await this.cryptoService.setKeyHash(hashedPassword); await this.userService.setUserIdAndEmail(this.tokenService.getUserId(), this.tokenService.getEmail()); - await this.cryptoService.setEncKey(response.key); - await this.cryptoService.setEncPrivateKey(response.privateKey); + await this.cryptoService.setEncKey(tokenResponse.key); + await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey); this.messagingService.send('loggedIn'); - return { - twoFactor: false, - twoFactorProviders: null, - }; + return result; } - logOut(callback: Function) { - callback(); + private clearState(): void { + this.email = null; + this.masterPasswordHash = null; + this.twoFactorProviders = null; } } diff --git a/src/services/cipher.service.ts b/src/services/cipher.service.ts index 9ab02b910c..fb8e532d62 100644 --- a/src/services/cipher.service.ts +++ b/src/services/cipher.service.ts @@ -434,7 +434,7 @@ export class CipherService implements CipherServiceAbstraction { let aName = a.name; let bName = b.name; - let result = this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : + const result = this.i18nService.collator ? this.i18nService.collator.compare(aName, bName) : aName.localeCompare(bName); if (result !== 0 || a.type !== CipherType.Login || b.type !== CipherType.Login) {