1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-10 21:33:17 +00:00

Break tokenRequest into subclasses

This commit is contained in:
Thomas Rittson
2021-12-16 15:32:44 +10:00
parent 6cb2b91fee
commit bd55e6ec81
10 changed files with 215 additions and 150 deletions

View File

@@ -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<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
postIdentityToken: (request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest) =>
Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
refreshIdentityToken: () => Promise<any>;
getProfile: () => Promise<ProfileResponse>;

View File

@@ -11,7 +11,7 @@ export abstract class TokenService {
toggleTokens: () => Promise<any>;
setTwoFactorToken: (token: string, email: string) => Promise<any>;
getTwoFactorToken: (email: string) => Promise<string>;
clearTwoFactorToken: (email: string) => Promise<any>;
clearTwoFactorToken: () => Promise<any>;
clearToken: (userId?: string) => Promise<any>;
decodeToken: (token?: string) => any;
getTokenExpirationDate: () => Promise<Date>;

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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
}
}

View File

@@ -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));
}
}
}

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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<CryptoService>;
@@ -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({