From bd55e6ec811e313cc2687ee6c0a494ef4fe86ce1 Mon Sep 17 00:00:00 2001 From: Thomas Rittson Date: Thu, 16 Dec 2021 15:32:44 +1000 Subject: [PATCH] Break tokenRequest into subclasses --- common/src/abstractions/api.service.ts | 8 +- common/src/abstractions/token.service.ts | 2 +- .../request/identityToken/apiTokenRequest.ts | 29 +++++++ .../identityToken/passwordTokenRequest.ts | 34 ++++++++ .../request/identityToken/ssoTokenRequest.ts | 31 +++++++ .../request/identityToken/tokenRequest.ts | 44 ++++++++++ common/src/models/request/tokenRequest.ts | 84 ------------------- common/src/services/api.service.ts | 6 +- common/src/services/auth.service.ts | 56 ++++++------- spec/common/services/auth.service.spec.ts | 71 +++++++++------- 10 files changed, 215 insertions(+), 150 deletions(-) create mode 100644 common/src/models/request/identityToken/apiTokenRequest.ts create mode 100644 common/src/models/request/identityToken/passwordTokenRequest.ts create mode 100644 common/src/models/request/identityToken/ssoTokenRequest.ts create mode 100644 common/src/models/request/identityToken/tokenRequest.ts delete mode 100644 common/src/models/request/tokenRequest.ts diff --git a/common/src/abstractions/api.service.ts b/common/src/abstractions/api.service.ts index 1c6aa0ef..02f17ae2 100644 --- a/common/src/abstractions/api.service.ts +++ b/common/src/abstractions/api.service.ts @@ -75,7 +75,6 @@ import { SendRequest } from '../models/request/sendRequest'; import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryRequest'; @@ -93,6 +92,10 @@ import { VerifyBankRequest } from '../models/request/verifyBankRequest'; import { VerifyDeleteRecoverRequest } from '../models/request/verifyDeleteRecoverRequest'; import { VerifyEmailRequest } from '../models/request/verifyEmailRequest'; +import { ApiTokenRequest } from '../models/request/identityToken/apiTokenRequest'; +import { PasswordTokenRequest } from '../models/request/identityToken/passwordTokenRequest'; +import { SsoTokenRequest } from '../models/request/identityToken/ssoTokenRequest'; + import { ApiKeyResponse } from '../models/response/apiKeyResponse'; import { AttachmentResponse } from '../models/response/attachmentResponse'; import { AttachmentUploadDataResponse } from '../models/response/attachmentUploadDataResponse'; @@ -164,7 +167,8 @@ import { UserKeyResponse } from '../models/response/userKeyResponse'; import { SendAccessView } from '../models/view/sendAccessView'; export abstract class ApiService { - postIdentityToken: (request: TokenRequest) => Promise; + postIdentityToken: (request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest) => + Promise; refreshIdentityToken: () => Promise; getProfile: () => Promise; diff --git a/common/src/abstractions/token.service.ts b/common/src/abstractions/token.service.ts index e4e2eb02..96b0f334 100644 --- a/common/src/abstractions/token.service.ts +++ b/common/src/abstractions/token.service.ts @@ -11,7 +11,7 @@ export abstract class TokenService { toggleTokens: () => Promise; setTwoFactorToken: (token: string, email: string) => Promise; getTwoFactorToken: (email: string) => Promise; - clearTwoFactorToken: (email: string) => Promise; + clearTwoFactorToken: () => Promise; clearToken: (userId?: string) => Promise; decodeToken: (token?: string) => any; getTokenExpirationDate: () => Promise; diff --git a/common/src/models/request/identityToken/apiTokenRequest.ts b/common/src/models/request/identityToken/apiTokenRequest.ts new file mode 100644 index 00000000..4ef901b0 --- /dev/null +++ b/common/src/models/request/identityToken/apiTokenRequest.ts @@ -0,0 +1,29 @@ +import { TokenRequest } from './tokenRequest'; + +import { TwoFactorProviderType } from '../../../enums/twoFactorProviderType'; + +import { DeviceRequest } from '../deviceRequest'; + +export class ApiTokenRequest extends TokenRequest { + clientId: string; + clientSecret: string; + + constructor(clientId: string, clientSecret: string, public provider: TwoFactorProviderType, + public token: string, public remember: boolean, public captchaResponse: string, device?: DeviceRequest) { + super(provider, token, remember, captchaResponse, device); + + this.clientId = clientId + this.clientSecret = clientSecret; + } + + toIdentityToken(clientId: string) { + const obj = super.toIdentityToken(clientId); + + obj.clientId = this.clientId; + obj.scope = clientId.startsWith('organization') ? 'api.organization' : 'api'; + obj.grant_type = 'client_credentials'; + obj.client_secret = this.clientSecret; + + return obj; + } +} diff --git a/common/src/models/request/identityToken/passwordTokenRequest.ts b/common/src/models/request/identityToken/passwordTokenRequest.ts new file mode 100644 index 00000000..de420a81 --- /dev/null +++ b/common/src/models/request/identityToken/passwordTokenRequest.ts @@ -0,0 +1,34 @@ +import { TokenRequest } from './tokenRequest'; + +import { TwoFactorProviderType } from '../../../enums/twoFactorProviderType'; + +import { DeviceRequest } from '../deviceRequest'; + +import { Utils } from '../../../misc/utils'; + +export class PasswordTokenRequest extends TokenRequest { + email: string; + masterPasswordHash: string; + + constructor(email: string, masterPasswordHash: string, public provider: TwoFactorProviderType, public token: string, + public remember: boolean, public captchaResponse: string, device?: DeviceRequest) { + super(provider, token, remember, captchaResponse, device); + + this.email = email; + this.masterPasswordHash = masterPasswordHash; + } + + toIdentityToken(clientId: string) { + const obj = super.toIdentityToken(clientId); + + obj.grant_type = 'password'; + obj.username = this.email; + obj.password = this.masterPasswordHash; + + return obj; + } + + alterIdentityTokenHeaders(headers: Headers) { + headers.set('Auth-Email', Utils.fromUtf8ToUrlB64(this.email)); + } +} diff --git a/common/src/models/request/identityToken/ssoTokenRequest.ts b/common/src/models/request/identityToken/ssoTokenRequest.ts new file mode 100644 index 00000000..f9068a29 --- /dev/null +++ b/common/src/models/request/identityToken/ssoTokenRequest.ts @@ -0,0 +1,31 @@ +import { TokenRequest } from './tokenRequest'; + +import { TwoFactorProviderType } from '../../../enums/twoFactorProviderType'; + +import { DeviceRequest } from '../deviceRequest'; + +export class SsoTokenRequest extends TokenRequest { + code: string; + codeVerifier: string; + redirectUri: string; + + constructor(code: string, codeVerifier: string, redirectUri: string, public provider: TwoFactorProviderType, + public token: string, public remember: boolean, public captchaResponse: string, device?: DeviceRequest) { + super(provider, token, remember, captchaResponse, device); + + this.code = code; + this.codeVerifier = codeVerifier; + this.redirectUri = redirectUri; + } + + toIdentityToken(clientId: string) { + const obj = super.toIdentityToken(clientId); + + obj.grant_type = 'authorization_code'; + obj.code = this.code; + obj.code_verifier = this.codeVerifier; + obj.redirect_uri = this.redirectUri; + + return obj; + } +} diff --git a/common/src/models/request/identityToken/tokenRequest.ts b/common/src/models/request/identityToken/tokenRequest.ts new file mode 100644 index 00000000..a3722b53 --- /dev/null +++ b/common/src/models/request/identityToken/tokenRequest.ts @@ -0,0 +1,44 @@ +import { TwoFactorProviderType } from '../../../enums/twoFactorProviderType'; + +import { CaptchaProtectedRequest } from '../captchaProtectedRequest'; +import { DeviceRequest } from '../deviceRequest'; + +export abstract class TokenRequest implements CaptchaProtectedRequest { + device?: DeviceRequest; + + constructor(public provider: TwoFactorProviderType, public token: string, public remember: boolean, + public captchaResponse: string, device?: DeviceRequest) { + this.device = device != null ? device : null; + } + + toIdentityToken(clientId: string) { + const obj: any = { + scope: 'api offline_access', + client_id: clientId, + }; + + if (this.device) { + obj.deviceType = this.device.type; + obj.deviceIdentifier = this.device.identifier; + obj.deviceName = this.device.name; + // no push tokens for browser apps yet + // obj.devicePushToken = this.device.pushToken; + } + + if (this.token && this.provider != null) { + obj.twoFactorToken = this.token; + obj.twoFactorProvider = this.provider; + obj.twoFactorRemember = this.remember ? '1' : '0'; + } + + if (this.captchaResponse != null) { + obj.captchaResponse = this.captchaResponse; + } + + return obj; + } + + alterIdentityTokenHeaders(headers: Headers) { + // Implemented in subclass if required + } +} diff --git a/common/src/models/request/tokenRequest.ts b/common/src/models/request/tokenRequest.ts deleted file mode 100644 index 41797eb0..00000000 --- a/common/src/models/request/tokenRequest.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; - -import { CaptchaProtectedRequest } from './captchaProtectedRequest'; -import { DeviceRequest } from './deviceRequest'; - -import { Utils } from '../../misc/utils'; - -export class TokenRequest implements CaptchaProtectedRequest { - email: string; - masterPasswordHash: string; - code: string; - codeVerifier: string; - redirectUri: string; - clientId: string; - clientSecret: string; - device?: DeviceRequest; - - constructor(credentials: string[], codes: string[], clientIdClientSecret: string[], public provider: TwoFactorProviderType, - public token: string, public remember: boolean, public captchaResponse: string, device?: DeviceRequest) { - 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]; - } else if (clientIdClientSecret != null && clientIdClientSecret.length > 1) { - this.clientId = clientIdClientSecret[0]; - this.clientSecret = clientIdClientSecret[1]; - } - this.device = device != null ? device : null; - } - - toIdentityToken(clientId: string) { - const obj: any = { - scope: 'api offline_access', - client_id: clientId, - }; - - if (this.clientSecret != null) { - obj.scope = clientId.startsWith('organization') ? 'api.organization' : 'api'; - obj.grant_type = 'client_credentials'; - obj.client_secret = this.clientSecret; - } else 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; - obj.deviceName = this.device.name; - // no push tokens for browser apps yet - // obj.devicePushToken = this.device.pushToken; - } - - if (this.token && this.provider != null) { - obj.twoFactorToken = this.token; - obj.twoFactorProvider = this.provider; - obj.twoFactorRemember = this.remember ? '1' : '0'; - } - - if (this.captchaResponse != null) { - obj.captchaResponse = this.captchaResponse; - } - - - return obj; - } - - alterIdentityTokenHeaders(headers: Headers) { - if (this.clientSecret == null && this.masterPasswordHash != null && this.email != null) { - headers.set('Auth-Email', Utils.fromUtf8ToUrlB64(this.email)); - } - } -} diff --git a/common/src/services/api.service.ts b/common/src/services/api.service.ts index f42a760e..5760bed0 100644 --- a/common/src/services/api.service.ts +++ b/common/src/services/api.service.ts @@ -76,7 +76,7 @@ import { SendRequest } from '../models/request/sendRequest'; import { SetPasswordRequest } from '../models/request/setPasswordRequest'; import { StorageRequest } from '../models/request/storageRequest'; import { TaxInfoUpdateRequest } from '../models/request/taxInfoUpdateRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; +import { TokenRequest } from '../models/request/identityToken/tokenRequest'; import { TwoFactorEmailRequest } from '../models/request/twoFactorEmailRequest'; import { TwoFactorProviderRequest } from '../models/request/twoFactorProviderRequest'; import { TwoFactorRecoveryRequest } from '../models/request/twoFactorRecoveryRequest'; @@ -209,7 +209,7 @@ export class ApiService implements ApiServiceAbstraction { } request.alterIdentityTokenHeaders(headers); const response = await this.fetch(new Request(this.environmentService.getIdentityUrl() + '/connect/token', { - body: this.qsStringify(request.toIdentityToken(request.clientId ?? this.platformUtilsService.identityClientId)), + body: this.qsStringify(request.toIdentityToken(this.platformUtilsService.identityClientId)), credentials: this.getCredentials(), cache: 'no-store', headers: headers, @@ -226,7 +226,7 @@ export class ApiService implements ApiServiceAbstraction { return new IdentityTokenResponse(responseJson); } else if (response.status === 400 && responseJson.TwoFactorProviders2 && Object.keys(responseJson.TwoFactorProviders2).length) { - await this.tokenService.clearTwoFactorToken(request.email); + await this.tokenService.clearTwoFactorToken(); return new IdentityTwoFactorResponse(responseJson); } else if (response.status === 400 && responseJson.HCaptcha_SiteKey && Object.keys(responseJson.HCaptcha_SiteKey).length) { diff --git a/common/src/services/auth.service.ts b/common/src/services/auth.service.ts index 3be57e06..75b87c19 100644 --- a/common/src/services/auth.service.ts +++ b/common/src/services/auth.service.ts @@ -11,7 +11,10 @@ import { DeviceRequest } from '../models/request/deviceRequest'; import { KeyConnectorUserKeyRequest } from '../models/request/keyConnectorUserKeyRequest'; import { KeysRequest } from '../models/request/keysRequest'; import { PreloginRequest } from '../models/request/preloginRequest'; -import { TokenRequest } from '../models/request/tokenRequest'; + +import { ApiTokenRequest } from '../models/request/identityToken/apiTokenRequest'; +import { PasswordTokenRequest } from '../models/request/identityToken/passwordTokenRequest'; +import { SsoTokenRequest } from '../models/request/identityToken/ssoTokenRequest'; import { IdentityTokenResponse } from '../models/response/identityTokenResponse'; import { IdentityTwoFactorResponse } from '../models/response/identityTwoFactorResponse'; @@ -142,6 +145,7 @@ export class AuthService implements AuthServiceAbstraction { } return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); } + private async logInHelper(email: string, hashedPassword: string, localHashedPassword: string, code: string, codeVerifier: string, redirectUrl: string, clientId: string, clientSecret: string, key: SymmetricCryptoKey, twoFactorProvider?: TwoFactorProviderType, twoFactorToken?: string, remember?: boolean, captchaToken?: string, @@ -223,39 +227,31 @@ export class AuthService implements AuthServiceAbstraction { const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const deviceRequest = new DeviceRequest(appId, this.platformUtilsService); - let emailPassword: string[] = []; - let codeCodeVerifier: string[] = []; - let clientIdClientSecret: [string, string] = [null, null]; + let effectiveToken = null; + let effectiveProvider = null; + let effectiveRemember = false; + + if (twoFactorToken != null && twoFactorProvider != null) { + effectiveToken = twoFactorToken; + effectiveProvider = twoFactorProvider; + effectiveRemember = remember; + } else if (storedTwoFactorToken != null) { + effectiveToken = storedTwoFactorToken; + effectiveProvider = TwoFactorProviderType.Remember; + } if (email != null && hashedPassword != null) { - emailPassword = [email, hashedPassword]; + return new PasswordTokenRequest(email, hashedPassword, effectiveProvider, effectiveToken, + effectiveRemember, captchaToken, deviceRequest); + } else if (code != null && codeVerifier != null && redirectUrl != null) { + return new SsoTokenRequest(code, codeVerifier, redirectUrl, effectiveProvider, effectiveToken, + effectiveRemember, captchaToken, deviceRequest); + } else if (clientId != null && clientSecret != null) { + return new ApiTokenRequest(clientId, clientSecret, effectiveProvider, effectiveToken, effectiveRemember, + captchaToken, deviceRequest); } else { - emailPassword = null; + throw new Error('No credentials provided.'); } - if (code != null && codeVerifier != null && redirectUrl != null) { - codeCodeVerifier = [code, codeVerifier, redirectUrl]; - } else { - codeCodeVerifier = null; - } - if (clientId != null && clientSecret != null) { - clientIdClientSecret = [clientId, clientSecret]; - } else { - clientIdClientSecret = null; - } - - let request: TokenRequest; - if (twoFactorToken != null && twoFactorProvider != null) { - request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, twoFactorProvider, - twoFactorToken, remember, captchaToken, deviceRequest); - } else if (storedTwoFactorToken != null) { - request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, - TwoFactorProviderType.Remember, storedTwoFactorToken, false, captchaToken, deviceRequest); - } else { - request = new TokenRequest(emailPassword, codeCodeVerifier, clientIdClientSecret, null, - null, false, captchaToken, deviceRequest); - } - - return request; } private async saveAccountInformation(tokenResponse: IdentityTokenResponse, clientId: string, clientSecret: string) { diff --git a/spec/common/services/auth.service.spec.ts b/spec/common/services/auth.service.spec.ts index 13ef8173..41314b5f 100644 --- a/spec/common/services/auth.service.spec.ts +++ b/spec/common/services/auth.service.spec.ts @@ -28,6 +28,9 @@ import { IdentityTokenResponse } from 'jslib-common/models/response/identityToke import { TwoFactorService } from 'jslib-common/abstractions/twoFactor.service'; import { HashPurpose } from 'jslib-common/enums/hashPurpose'; import { TwoFactorProviderType } from 'jslib-common/enums/twoFactorProviderType'; +import { SsoTokenRequest } from 'jslib-common/models/request/identityToken/ssoTokenRequest'; +import { ApiTokenRequest } from 'jslib-common/models/request/identityToken/apiTokenRequest'; +import { PasswordTokenRequest } from 'jslib-common/models/request/identityToken/passwordTokenRequest'; describe('Cipher Service', () => { let cryptoService: SubstituteOf; @@ -178,13 +181,15 @@ describe('Cipher Service', () => { // Assert // Api call: - apiService.received(1).postIdentityToken(Arg.is(actual => - actual.email === email && - actual.masterPasswordHash === hashedPassword && - actual.device.identifier === deviceId && - actual.provider == null && - actual.token == null && - actual.captchaResponse == null)); + apiService.received(1).postIdentityToken(Arg.is(actual => { + const passwordTokenRequest = actual as PasswordTokenRequest; + return passwordTokenRequest.email === email && + passwordTokenRequest.masterPasswordHash === hashedPassword && + actual.device.identifier === deviceId && + actual.provider == null && + actual.token == null && + actual.captchaResponse == null + })); // Sets local environment: commonSuccessAssertions(); @@ -302,14 +307,16 @@ describe('Cipher Service', () => { await authService.logInTwoFactor(twoFactorProviderType, twoFactorToken, twoFactorRemember); - apiService.received(1).postIdentityToken(Arg.is(actual => - actual.email === email && - actual.masterPasswordHash === hashedPassword && - actual.device.identifier === deviceId && - actual.provider === twoFactorProviderType && - actual.token === twoFactorToken && - actual.remember === twoFactorRemember && - actual.captchaResponse == null)); + apiService.received(1).postIdentityToken(Arg.is(actual => { + const passwordTokenRequest = actual as PasswordTokenRequest; + return passwordTokenRequest.email === email && + passwordTokenRequest.masterPasswordHash === hashedPassword && + actual.device.identifier === deviceId && + actual.provider === twoFactorProviderType && + actual.token === twoFactorToken && + actual.remember === twoFactorRemember && + actual.captchaResponse == null + })); }); // SSO @@ -325,14 +332,16 @@ describe('Cipher Service', () => { // Assert // Api call: - apiService.received(1).postIdentityToken(Arg.is(actual => - actual.code === ssoCode && - actual.codeVerifier === ssoCodeVerifier && - actual.redirectUri === ssoRedirectUrl && - actual.device.identifier === deviceId && - actual.provider == null && - actual.token == null && - actual.captchaResponse == null)); + apiService.received(1).postIdentityToken(Arg.is(actual => { + const ssoTokenRequest = actual as SsoTokenRequest; + return ssoTokenRequest.code === ssoCode && + ssoTokenRequest.codeVerifier === ssoCodeVerifier && + ssoTokenRequest.redirectUri === ssoRedirectUrl && + actual.device.identifier === deviceId && + actual.provider == null && + actual.token == null && + actual.captchaResponse == null + })); // Sets local environment: commonSuccessAssertions(); @@ -427,13 +436,15 @@ describe('Cipher Service', () => { const result = await authService.logInApiKey(apiClientId, apiClientSecret); - apiService.received(1).postIdentityToken(Arg.is(actual => - actual.clientId === apiClientId && - actual.clientSecret === apiClientSecret && - actual.device.identifier === deviceId && - actual.provider == null && - actual.token == null && - actual.captchaResponse == null)); + apiService.received(1).postIdentityToken(Arg.is(actual => { + const apiTokenRequest = actual as ApiTokenRequest; + return apiTokenRequest.clientId === apiClientId && + apiTokenRequest.clientSecret === apiClientSecret && + actual.device.identifier === deviceId && + actual.provider == null && + actual.token == null && + actual.captchaResponse == null + })); // Sets local environment: stateService.received(1).addAccount({