From fefef546f0f9e11377203df47de44a54809ca655 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 16 Jul 2020 08:59:29 -0400 Subject: [PATCH] sso support (#127) * support for sso * created master password boolean * resetMasterPassword flows * throw on bad ctor for token request --- src/abstractions/api.service.ts | 1 + src/abstractions/auth.service.ts | 6 ++ src/angular/components/lock.component.ts | 28 ++++++- .../components/two-factor.component.ts | 6 +- src/misc/utils.ts | 8 ++ src/models/domain/authResult.ts | 1 + src/models/request/tokenRequest.ts | 31 +++++-- src/models/response/identityTokenResponse.ts | 8 ++ src/services/api.service.ts | 4 + src/services/auth.service.ts | 80 ++++++++++++++----- src/services/constants.service.ts | 4 + 11 files changed, 148 insertions(+), 29 deletions(-) diff --git a/src/abstractions/api.service.ts b/src/abstractions/api.service.ts index 337b260b9e..b345e74fce 100644 --- a/src/abstractions/api.service.ts +++ b/src/abstractions/api.service.ts @@ -141,6 +141,7 @@ export abstract class ApiService { postAccountKeys: (request: KeysRequest) => Promise; postAccountVerifyEmail: () => Promise; postAccountVerifyEmailToken: (request: VerifyEmailRequest) => Promise; + postAccountVerifyPassword: (request: PasswordVerificationRequest) => Promise; postAccountRecoverDelete: (request: DeleteRecoverRequest) => Promise; postAccountRecoverDeleteToken: (request: VerifyDeleteRecoverRequest) => Promise; postAccountKdf: (request: KdfRequest) => Promise; diff --git a/src/abstractions/auth.service.ts b/src/abstractions/auth.service.ts index de7481ce60..21bba19633 100644 --- a/src/abstractions/auth.service.ts +++ b/src/abstractions/auth.service.ts @@ -6,10 +6,14 @@ import { SymmetricCryptoKey } from '../models/domain/symmetricCryptoKey'; export abstract class AuthService { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + ssoRedirectUrl: string; twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType; logIn: (email: string, masterPassword: string) => Promise; + logInSso: (code: string, codeVerifier: string, redirectUrl: string) => Promise; logInTwoFactor: (twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean) => Promise; logInComplete: (email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -18,4 +22,6 @@ export abstract class AuthService { getSupportedTwoFactorProviders: (win: Window) => any[]; getDefaultTwoFactorProvider: (u2fSupported: boolean) => TwoFactorProviderType; makePreloginKey: (masterPassword: string, email: string) => Promise; + authingWithSso: () => boolean; + authingWithPassword: () => boolean; } diff --git a/src/angular/components/lock.component.ts b/src/angular/components/lock.component.ts index 107ab47191..0cf22362c3 100644 --- a/src/angular/components/lock.component.ts +++ b/src/angular/components/lock.component.ts @@ -1,6 +1,7 @@ import { OnInit } from '@angular/core'; import { Router } from '@angular/router'; +import { ApiService } from '../../abstractions/api.service'; import { CryptoService } from '../../abstractions/crypto.service'; import { EnvironmentService } from '../../abstractions/environment.service'; import { I18nService } from '../../abstractions/i18n.service'; @@ -16,6 +17,8 @@ import { ConstantsService } from '../../services/constants.service'; import { CipherString } from '../../models/domain/cipherString'; import { SymmetricCryptoKey } from '../../models/domain/symmetricCryptoKey'; +import { PasswordVerificationRequest } from '../../models/request/passwordVerificationRequest'; + import { Utils } from '../../misc/utils'; export class LockComponent implements OnInit { @@ -25,6 +28,7 @@ export class LockComponent implements OnInit { email: string; pinLock: boolean = false; webVaultHostname: string = ''; + formPromise: Promise; protected successRoute: string = 'vault'; protected onSuccessfulSubmit: () => void; @@ -36,7 +40,8 @@ export class LockComponent implements OnInit { protected platformUtilsService: PlatformUtilsService, protected messagingService: MessagingService, protected userService: UserService, protected cryptoService: CryptoService, protected storageService: StorageService, protected vaultTimeoutService: VaultTimeoutService, - protected environmentService: EnvironmentService, protected stateService: StateService) { } + protected environmentService: EnvironmentService, protected stateService: StateService, + protected apiService: ApiService) { } async ngOnInit() { this.pinSet = await this.vaultTimeoutService.isPinLockSet(); @@ -98,9 +103,26 @@ export class LockComponent implements OnInit { } else { const key = await this.cryptoService.makeKey(this.masterPassword, this.email, kdf, kdfIterations); const keyHash = await this.cryptoService.hashPassword(this.masterPassword, key); - const storedKeyHash = await this.cryptoService.getKeyHash(); - if (storedKeyHash != null && keyHash != null && storedKeyHash === keyHash) { + let passwordValid = false; + + if (keyHash != null) { + const storedKeyHash = await this.cryptoService.getKeyHash(); + if (storedKeyHash != null) { + passwordValid = storedKeyHash === keyHash; + } else { + const request = new PasswordVerificationRequest(); + request.masterPasswordHash = keyHash; + try { + this.formPromise = this.apiService.postAccountVerifyPassword(request); + await this.formPromise; + passwordValid = true; + await this.cryptoService.setKeyHash(keyHash); + } catch { } + } + } + + if (passwordValid) { if (this.pinSet[0]) { const protectedPin = await this.storageService.get(ConstantsService.protectedPin); const encKey = await this.cryptoService.getEncKey(key); diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts index 9d3379405c..94a1e1553a 100644 --- a/src/angular/components/two-factor.component.ts +++ b/src/angular/components/two-factor.component.ts @@ -52,12 +52,16 @@ export class TwoFactorComponent implements OnInit, OnDestroy { } async ngOnInit() { - if (this.authService.email == null || this.authService.masterPasswordHash == null || + if ((!this.authService.authingWithSso() && !this.authService.authingWithPassword()) || this.authService.twoFactorProvidersData == null) { this.router.navigate([this.loginRoute]); return; } + if (this.authService.authingWithSso()) { + this.successRoute = 'lock'; + } + if (this.initU2f && this.win != null && this.u2fSupported) { let customWebVaultUrl: string = null; if (this.environmentService.baseUrl != null) { diff --git a/src/misc/utils.ts b/src/misc/utils.ts index e9b8b4911d..4d9bf8533e 100644 --- a/src/misc/utils.ts +++ b/src/misc/utils.ts @@ -89,6 +89,14 @@ export class Utils { } } + static fromBufferToUrlB64(buffer: ArrayBuffer): string { + const output = this.fromBufferToB64(buffer) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return output; + } + static fromBufferToUtf8(buffer: ArrayBuffer): string { if (Utils.isNode || Utils.isNativeScript) { return Buffer.from(buffer).toString('utf8'); diff --git a/src/models/domain/authResult.ts b/src/models/domain/authResult.ts index cb4bb57c65..0f5b95a521 100644 --- a/src/models/domain/authResult.ts +++ b/src/models/domain/authResult.ts @@ -2,5 +2,6 @@ import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; export class AuthResult { twoFactor: boolean = false; + resetMasterPassword: boolean = false; twoFactorProviders: Map = null; } diff --git a/src/models/request/tokenRequest.ts b/src/models/request/tokenRequest.ts index c9bcd5ad36..f0ff702428 100644 --- a/src/models/request/tokenRequest.ts +++ b/src/models/request/tokenRequest.ts @@ -5,15 +5,24 @@ import { DeviceRequest } from './deviceRequest'; export class TokenRequest { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + redirectUri: string; token: string; provider: TwoFactorProviderType; remember: boolean; device?: DeviceRequest; - constructor(email: string, masterPasswordHash: string, provider: TwoFactorProviderType, + constructor(credentials: string[], codes: string[], provider: TwoFactorProviderType, token: string, remember: boolean, device?: DeviceRequest) { - this.email = email; - this.masterPasswordHash = masterPasswordHash; + if (credentials != null && credentials.length > 1) { + this.email = credentials[0]; + this.masterPasswordHash = credentials[1]; + } else if (codes != null && codes.length > 2) { + this.code = codes[0]; + this.codeVerifier = codes[1]; + this.redirectUri = codes[2]; + } this.token = token; this.provider = provider; this.remember = remember; @@ -22,13 +31,23 @@ export class TokenRequest { toIdentityToken(clientId: string) { const obj: any = { - grant_type: 'password', - username: this.email, - password: this.masterPasswordHash, scope: 'api offline_access', client_id: clientId, }; + if (this.masterPasswordHash != null && this.email != null) { + obj.grant_type = 'password'; + obj.username = this.email; + obj.password = this.masterPasswordHash; + } else if (this.code != null && this.codeVerifier != null && this.redirectUri != null) { + obj.grant_type = 'authorization_code'; + obj.code = this.code; + obj.code_verifier = this.codeVerifier; + obj.redirect_uri = this.redirectUri; + } else { + throw new Error('must provide credentials or codes'); + } + if (this.device) { obj.deviceType = this.device.type; obj.deviceIdentifier = this.device.identifier; diff --git a/src/models/response/identityTokenResponse.ts b/src/models/response/identityTokenResponse.ts index 20adaff718..7ce1afba0c 100644 --- a/src/models/response/identityTokenResponse.ts +++ b/src/models/response/identityTokenResponse.ts @@ -1,14 +1,19 @@ import { BaseResponse } from './baseResponse'; +import { KdfType } from '../../enums/kdfType'; + export class IdentityTokenResponse extends BaseResponse { accessToken: string; expiresIn: number; refreshToken: string; tokenType: string; + resetMasterPassword: boolean; privateKey: string; key: string; twoFactorToken: string; + kdf: KdfType; + kdfIterations: number; constructor(response: any) { super(response); @@ -17,8 +22,11 @@ export class IdentityTokenResponse extends BaseResponse { this.refreshToken = response.refresh_token; this.tokenType = response.token_type; + this.resetMasterPassword = this.getResponseProperty('ResetMasterPassword'); this.privateKey = this.getResponseProperty('PrivateKey'); this.key = this.getResponseProperty('Key'); this.twoFactorToken = this.getResponseProperty('TwoFactorToken'); + this.kdf = this.getResponseProperty('Kdf'); + this.kdfIterations = this.getResponseProperty('KdfIterations'); } } diff --git a/src/services/api.service.ts b/src/services/api.service.ts index f86a4efcff..8b9249429e 100644 --- a/src/services/api.service.ts +++ b/src/services/api.service.ts @@ -321,6 +321,10 @@ export class ApiService implements ApiServiceAbstraction { return this.send('POST', '/accounts/verify-email-token', request, false, false); } + postAccountVerifyPassword(request: PasswordVerificationRequest): Promise { + return this.send('POST', '/accounts/verify-password', request, true, false); + } + postAccountRecoverDelete(request: DeleteRecoverRequest): Promise { return this.send('POST', '/accounts/delete-recover', request, false, false); } diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index ad379f29fd..9a8d0ec3e0 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -77,12 +77,13 @@ export const TwoFactorProviders = { export class AuthService implements AuthServiceAbstraction { email: string; masterPasswordHash: string; + code: string; + codeVerifier: string; + ssoRedirectUrl: string; twoFactorProvidersData: Map; selectedTwoFactorProviderType: TwoFactorProviderType = null; private key: SymmetricCryptoKey; - private kdf: KdfType; - private kdfIterations: number; constructor(private cryptoService: CryptoService, private apiService: ApiService, private userService: UserService, private tokenService: TokenService, @@ -116,13 +117,19 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - return await this.logInHelper(email, hashedPassword, key); + return await this.logInHelper(email, hashedPassword, null, null, null, key, + null, null, null); + } + + async logInSso(code: string, codeVerifier: string, redirectUrl: string): Promise { + this.selectedTwoFactorProviderType = null; + return await this.logInHelper(null, null, code, codeVerifier, redirectUrl, null, null, null, null); } async logInTwoFactor(twoFactorProvider: TwoFactorProviderType, twoFactorToken: string, remember?: boolean): Promise { - return await this.logInHelper(this.email, this.masterPasswordHash, this.key, twoFactorProvider, - twoFactorToken, remember); + return await this.logInHelper(this.email, this.masterPasswordHash, this.code, this.codeVerifier, + this.ssoRedirectUrl, this.key, twoFactorProvider, twoFactorToken, remember); } async logInComplete(email: string, masterPassword: string, twoFactorProvider: TwoFactorProviderType, @@ -130,7 +137,8 @@ export class AuthService implements AuthServiceAbstraction { this.selectedTwoFactorProviderType = null; const key = await this.makePreloginKey(masterPassword, email); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); - return await this.logInHelper(email, hashedPassword, key, twoFactorProvider, twoFactorToken, remember); + return await this.logInHelper(email, hashedPassword, null, null, null, key, twoFactorProvider, twoFactorToken, + remember); } logOut(callback: Function) { @@ -201,37 +209,59 @@ export class AuthService implements AuthServiceAbstraction { async makePreloginKey(masterPassword: string, email: string): Promise { email = email.trim().toLowerCase(); - this.kdf = null; - this.kdfIterations = null; + let kdf: KdfType = null; + let kdfIterations: number = null; try { const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email)); if (preloginResponse != null) { - this.kdf = preloginResponse.kdf; - this.kdfIterations = preloginResponse.kdfIterations; + kdf = preloginResponse.kdf; + kdfIterations = preloginResponse.kdfIterations; } } catch (e) { if (e == null || e.statusCode !== 404) { throw e; } } - return this.cryptoService.makeKey(masterPassword, email, this.kdf, this.kdfIterations); + return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); } - private async logInHelper(email: string, hashedPassword: string, key: SymmetricCryptoKey, - twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean): Promise { + authingWithSso(): boolean { + return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null; + } + + authingWithPassword(): boolean { + return this.email != null && this.masterPasswordHash != null; + } + + private async logInHelper(email: string, hashedPassword: string, code: string, codeVerifier: string, + redirectUrl: 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 emailPassword: string[] = []; + let codeCodeVerifier: string[] = []; + if (email != null && hashedPassword != null) { + emailPassword = [email, hashedPassword]; + } else { + emailPassword = null; + } + if (code != null && codeVerifier != null && redirectUrl != null) { + codeCodeVerifier = [code, codeVerifier, redirectUrl]; + } else { + codeCodeVerifier = null; + } + let request: TokenRequest; if (twoFactorToken != null && twoFactorProvider != null) { - request = new TokenRequest(email, hashedPassword, twoFactorProvider, twoFactorToken, remember, + request = new TokenRequest(emailPassword, codeCodeVerifier, twoFactorProvider, twoFactorToken, remember, deviceRequest); } else if (storedTwoFactorToken != null) { - request = new TokenRequest(email, hashedPassword, TwoFactorProviderType.Remember, + request = new TokenRequest(emailPassword, codeCodeVerifier, TwoFactorProviderType.Remember, storedTwoFactorToken, false, deviceRequest); } else { - request = new TokenRequest(email, hashedPassword, null, null, false, deviceRequest); + request = new TokenRequest(emailPassword, codeCodeVerifier, null, null, false, deviceRequest); } const response = await this.apiService.postIdentityToken(request); @@ -245,6 +275,9 @@ export class AuthService implements AuthServiceAbstraction { const twoFactorResponse = response as IdentityTwoFactorResponse; this.email = email; this.masterPasswordHash = hashedPassword; + this.code = code; + this.codeVerifier = codeVerifier; + this.ssoRedirectUrl = redirectUrl; this.key = this.setCryptoKeys ? key : null; this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2; result.twoFactorProviders = twoFactorResponse.twoFactorProviders2; @@ -252,16 +285,21 @@ export class AuthService implements AuthServiceAbstraction { } const tokenResponse = response as IdentityTokenResponse; + result.resetMasterPassword = tokenResponse.resetMasterPassword; if (tokenResponse.twoFactorToken != null) { await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email); } await this.tokenService.setTokens(tokenResponse.accessToken, tokenResponse.refreshToken); await this.userService.setInformation(this.tokenService.getUserId(), this.tokenService.getEmail(), - this.kdf, this.kdfIterations); + tokenResponse.kdf, tokenResponse.kdfIterations); if (this.setCryptoKeys) { - await this.cryptoService.setKey(key); - await this.cryptoService.setKeyHash(hashedPassword); + if (key != null) { + await this.cryptoService.setKey(key); + } + if (hashedPassword != null) { + await this.cryptoService.setKeyHash(hashedPassword); + } await this.cryptoService.setEncKey(tokenResponse.key); // User doesn't have a key pair yet (old account), let's generate one for them @@ -284,8 +322,12 @@ export class AuthService implements AuthServiceAbstraction { } private clearState(): void { + this.key = null; this.email = null; this.masterPasswordHash = null; + this.code = null; + this.codeVerifier = null; + this.ssoRedirectUrl = null; this.twoFactorProvidersData = null; this.selectedTwoFactorProviderType = null; } diff --git a/src/services/constants.service.ts b/src/services/constants.service.ts index 396ebffa3f..752648fd79 100644 --- a/src/services/constants.service.ts +++ b/src/services/constants.service.ts @@ -23,6 +23,8 @@ export class ConstantsService { static readonly protectedPin: string = 'protectedPin'; static readonly clearClipboardKey: string = 'clearClipboardKey'; static readonly eventCollectionKey: string = 'eventCollection'; + static readonly ssoCodeVerifierKey: string = 'ssoCodeVerifier'; + static readonly ssoStateKey: string = 'ssoState'; readonly environmentUrlsKey: string = ConstantsService.environmentUrlsKey; readonly disableGaKey: string = ConstantsService.disableGaKey; @@ -47,4 +49,6 @@ export class ConstantsService { readonly protectedPin: string = ConstantsService.protectedPin; readonly clearClipboardKey: string = ConstantsService.clearClipboardKey; readonly eventCollectionKey: string = ConstantsService.eventCollectionKey; + readonly ssoCodeVerifierKey: string = ConstantsService.ssoCodeVerifierKey; + readonly ssoStateKey: string = ConstantsService.ssoStateKey; }