1
0
mirror of https://github.com/bitwarden/jslib synced 2025-12-17 16:53:20 +00:00

Lift tokenRequest and api call to request methods

Also reduce amount of persistent state (WIP)
This commit is contained in:
Thomas Rittson
2021-12-20 09:20:46 +10:00
parent bb04c5bf86
commit 208e88800b
7 changed files with 134 additions and 174 deletions

View File

@@ -3,14 +3,6 @@ import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
import { TwoFactorData } from '../models/request/identityToken/tokenRequest'; import { TwoFactorData } from '../models/request/identityToken/tokenRequest';
export abstract class AuthService { export abstract class AuthService {
email: string;
masterPasswordHash: string;
code: string;
codeVerifier: string;
ssoRedirectUrl: string;
clientId: string;
clientSecret: string;
logIn: (email: string, masterPassword: string, twoFactor?: TwoFactorData, captchaToken?: string) => Promise<AuthResult>; logIn: (email: string, masterPassword: string, twoFactor?: TwoFactorData, captchaToken?: string) => Promise<AuthResult>;
logInSso: ( logInSso: (
code: string, code: string,

View File

@@ -4,8 +4,8 @@ import { DeviceRequest } from "../deviceRequest";
export class ApiTokenRequest extends TokenRequest { export class ApiTokenRequest extends TokenRequest {
constructor( constructor(
private clientId: string, public clientId: string,
private clientSecret: string, public clientSecret: string,
protected twoFactor: TwoFactorData, protected twoFactor: TwoFactorData,
captchaResponse: string, captchaResponse: string,
device?: DeviceRequest device?: DeviceRequest

View File

@@ -6,16 +6,13 @@ import { Utils } from "../../../misc/utils";
export class PasswordTokenRequest extends TokenRequest { export class PasswordTokenRequest extends TokenRequest {
constructor( constructor(
private email: string, public email: string,
private masterPasswordHash: string, public masterPasswordHash: string,
protected twoFactor: TwoFactorData, protected twoFactor: TwoFactorData,
captchaResponse: string, captchaResponse: string,
device?: DeviceRequest device?: DeviceRequest
) { ) {
super(twoFactor, captchaResponse, device); super(twoFactor, captchaResponse, device);
this.email = email;
this.masterPasswordHash = masterPasswordHash;
} }
toIdentityToken(clientId: string) { toIdentityToken(clientId: string) {

View File

@@ -4,18 +4,14 @@ import { DeviceRequest } from "../deviceRequest";
export class SsoTokenRequest extends TokenRequest { export class SsoTokenRequest extends TokenRequest {
constructor( constructor(
private code: string, public code: string,
private codeVerifier: string, public codeVerifier: string,
private redirectUri: string, public redirectUri: string,
protected twoFactor: TwoFactorData, protected twoFactor: TwoFactorData,
captchaResponse: string, captchaResponse: string,
device?: DeviceRequest device?: DeviceRequest
) { ) {
super(twoFactor, captchaResponse, device); super(twoFactor, captchaResponse, device);
this.code = code;
this.codeVerifier = codeVerifier;
this.redirectUri = redirectUri;
} }
toIdentityToken(clientId: string) { toIdentityToken(clientId: string) {

View File

@@ -50,4 +50,8 @@ export abstract class TokenRequest implements CaptchaProtectedRequest {
alterIdentityTokenHeaders(headers: Headers) { alterIdentityTokenHeaders(headers: Headers) {
// Implemented in subclass if required // Implemented in subclass if required
} }
setTwoFactor(twoFactor: TwoFactorData) {
this.twoFactor = twoFactor;
}
} }

View File

@@ -35,17 +35,12 @@ import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/twoFactor.service"; import { TwoFactorService } from "../abstractions/twoFactor.service";
import { Utils } from "../misc/utils"; import { Utils } from "../misc/utils";
import { IdentityCaptchaResponse } from "../models/response/identityCaptchaResponse";
export class AuthService implements AuthServiceAbstraction { export class AuthService implements AuthServiceAbstraction {
email: string; private localMasterPasswordHash: string;
masterPasswordHash: string;
localMasterPasswordHash: string; private savedTokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
code: string;
codeVerifier: string;
ssoRedirectUrl: string;
clientId: string;
clientSecret: string;
captchaToken: string;
private key: SymmetricCryptoKey; private key: SymmetricCryptoKey;
@@ -65,7 +60,12 @@ export class AuthService implements AuthServiceAbstraction {
private setCryptoKeys = true private setCryptoKeys = true
) {} ) {}
async logIn(email: string, masterPassword: string, twoFactor?: TwoFactorData, captchaToken?: string): Promise<AuthResult> { async logIn(
email: string,
masterPassword: string,
twoFactor?: TwoFactorData,
captchaToken?: string
): Promise<AuthResult> {
this.twoFactorService.clearSelectedProvider(); this.twoFactorService.clearSelectedProvider();
const key = await this.makePreloginKey(masterPassword, email); const key = await this.makePreloginKey(masterPassword, email);
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key); const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
@@ -74,20 +74,32 @@ export class AuthService implements AuthServiceAbstraction {
key, key,
HashPurpose.LocalAuthorization HashPurpose.LocalAuthorization
); );
return await this.logInHelper(
const tokenRequest = new PasswordTokenRequest(
email, email,
hashedPassword, hashedPassword,
await this.createTwoFactorData(twoFactor, email),
captchaToken,
await this.createDeviceRequest()
);
const response = await this.apiService.postIdentityToken(tokenRequest);
const result = await this.processTokenResponse(
response,
email,
localHashedPassword, localHashedPassword,
null, null,
null, null,
null, null,
null,
null,
key, key,
twoFactor,
captchaToken,
null null
); );
if (result.twoFactor) {
this.saveState(tokenRequest, key, localHashedPassword, result.twoFactorProviders);
}
return result;
} }
async logInSso( async logInSso(
@@ -95,59 +107,84 @@ export class AuthService implements AuthServiceAbstraction {
codeVerifier: string, codeVerifier: string,
redirectUrl: string, redirectUrl: string,
orgId: string, orgId: string,
twoFactor?: TwoFactorData, twoFactor?: TwoFactorData
): Promise<AuthResult> { ): Promise<AuthResult> {
this.twoFactorService.clearSelectedProvider(); this.twoFactorService.clearSelectedProvider();
return await this.logInHelper(
null, const tokenRequest = new SsoTokenRequest(
null,
null,
code, code,
codeVerifier, codeVerifier,
redirectUrl, redirectUrl,
await this.createTwoFactorData(twoFactor, null),
null,
await this.createDeviceRequest()
);
const response = await this.apiService.postIdentityToken(tokenRequest);
const result = await this.processTokenResponse(
response,
null, null,
null, null,
code,
null,
null, null,
twoFactor,
null, null,
orgId orgId
); );
if (result.twoFactor) {
this.saveState(tokenRequest, null, null, result.twoFactorProviders);
}
return result;
} }
async logInApiKey(clientId: string, clientSecret: string, twoFactor?: TwoFactorData): Promise<AuthResult> { async logInApiKey(
clientId: string,
clientSecret: string,
twoFactor?: TwoFactorData
): Promise<AuthResult> {
this.twoFactorService.clearSelectedProvider(); this.twoFactorService.clearSelectedProvider();
return await this.logInHelper(
null, const tokenRequest = new ApiTokenRequest(
null, clientId,
clientSecret,
await this.createTwoFactorData(twoFactor, null),
null, null,
await this.createDeviceRequest()
);
const response = await this.apiService.postIdentityToken(tokenRequest);
const result = await this.processTokenResponse(
response,
null, null,
null, null,
null, null,
clientId, clientId,
clientSecret, clientSecret,
null, null,
twoFactor,
null,
null null
); );
if (result.twoFactor) {
this.saveState(tokenRequest, null, null, result.twoFactorProviders);
}
return result;
} }
async logInTwoFactor( async logInTwoFactor(twoFactor: TwoFactorData): Promise<AuthResult> {
twoFactor: TwoFactorData, this.savedTokenRequest.setTwoFactor(twoFactor);
): Promise<AuthResult> { const response = await this.apiService.postIdentityToken(this.savedTokenRequest);
return await this.logInHelper(
this.email, return await this.processTokenResponse(
this.masterPasswordHash, response,
(this.savedTokenRequest as PasswordTokenRequest).email,
this.localMasterPasswordHash, this.localMasterPasswordHash,
this.code, (this.savedTokenRequest as SsoTokenRequest).code,
this.codeVerifier, (this.savedTokenRequest as ApiTokenRequest).clientId,
this.ssoRedirectUrl, (this.savedTokenRequest as ApiTokenRequest).clientSecret,
this.clientId, this.key
this.clientSecret,
this.key,
twoFactor,
this.captchaToken,
null
); );
} }
@@ -156,16 +193,20 @@ export class AuthService implements AuthServiceAbstraction {
this.messagingService.send("loggedOut"); this.messagingService.send("loggedOut");
} }
// TODO
authingWithApiKey(): boolean { authingWithApiKey(): boolean {
return this.clientId != null && this.clientSecret != null; return null;
// return this.clientId != null && this.clientSecret != null;
} }
authingWithSso(): boolean { authingWithSso(): boolean {
return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null; return null;
// return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null;
} }
authingWithPassword(): boolean { authingWithPassword(): boolean {
return this.email != null && this.masterPasswordHash != null; return null;
// return this.email != null && this.masterPasswordHash != null;
} }
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> { async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
@@ -186,34 +227,16 @@ export class AuthService implements AuthServiceAbstraction {
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations); return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
} }
private async logInHelper( private async processTokenResponse(
response: IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse,
email: string, email: string,
hashedPassword: string,
localHashedPassword: string, localHashedPassword: string,
code: string, code: string,
codeVerifier: string,
redirectUrl: string,
clientId: string, clientId: string,
clientSecret: string, clientSecret: string,
key: SymmetricCryptoKey, key: SymmetricCryptoKey,
twoFactor: TwoFactorData,
captchaToken?: string,
orgId?: string orgId?: string
): Promise<AuthResult> { ): Promise<AuthResult> {
const request = await this.createTokenRequest(
email,
hashedPassword,
code,
codeVerifier,
redirectUrl,
clientId,
clientSecret,
twoFactor,
captchaToken
);
const response = await this.apiService.postIdentityToken(request);
this.clearState(); this.clearState();
const result = new AuthResult(); const result = new AuthResult();
@@ -224,19 +247,6 @@ export class AuthService implements AuthServiceAbstraction {
result.twoFactor = !!(response as any).twoFactorProviders2; result.twoFactor = !!(response as any).twoFactorProviders2;
if (result.twoFactor) { if (result.twoFactor) {
this.saveState(
email,
hashedPassword,
localHashedPassword,
code,
codeVerifier,
redirectUrl,
clientId,
clientSecret,
key,
(response as IdentityTwoFactorResponse).twoFactorProviders2
);
result.twoFactorProviders = (response as IdentityTwoFactorResponse).twoFactorProviders2; result.twoFactorProviders = (response as IdentityTwoFactorResponse).twoFactorProviders2;
return result; return result;
} }
@@ -292,19 +302,7 @@ export class AuthService implements AuthServiceAbstraction {
return new DeviceRequest(appId, this.platformUtilsService); return new DeviceRequest(appId, this.platformUtilsService);
} }
private async createTokenRequest( private async createTwoFactorData(twoFactor: TwoFactorData, email: string) {
email: string,
hashedPassword: string,
code: string,
codeVerifier: string,
redirectUrl: string,
clientId: string,
clientSecret: string,
twoFactor: TwoFactorData,
captchaToken: string
) {
const deviceRequest = await this.createDeviceRequest();
if (twoFactor == null) { if (twoFactor == null) {
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email); const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email);
if (storedTwoFactorToken != null) { if (storedTwoFactorToken != null) {
@@ -312,38 +310,16 @@ export class AuthService implements AuthServiceAbstraction {
token: storedTwoFactorToken, token: storedTwoFactorToken,
provider: TwoFactorProviderType.Remember, provider: TwoFactorProviderType.Remember,
remember: false, remember: false,
} };
} else { } else {
twoFactor = { twoFactor = {
token: null, token: null,
provider: null, provider: null,
remember: false, remember: false,
}; };
}
} }
if (email != null && hashedPassword != null) {
return new PasswordTokenRequest(
email,
hashedPassword,
twoFactor,
captchaToken,
deviceRequest
);
} else if (code != null && codeVerifier != null && redirectUrl != null) {
return new SsoTokenRequest(
code,
codeVerifier,
redirectUrl,
twoFactor,
captchaToken,
deviceRequest
);
} else if (clientId != null && clientSecret != null) {
return new ApiTokenRequest(clientId, clientSecret, twoFactor, captchaToken, deviceRequest);
} else {
throw new Error("No credentials provided.");
} }
return twoFactor;
} }
private async saveAccountInformation( private async saveAccountInformation(
@@ -425,39 +401,21 @@ export class AuthService implements AuthServiceAbstraction {
} }
private saveState( private saveState(
email: string, tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest,
hashedPassword: string,
localHashedPassword: string,
code: string,
codeVerifier: string,
redirectUrl: string,
clientId: string,
clientSecret: string,
key: SymmetricCryptoKey, key: SymmetricCryptoKey,
localMasterPasswordHash: string,
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }>
) { ) {
this.email = email; this.savedTokenRequest = tokenRequest;
this.masterPasswordHash = hashedPassword; this.localMasterPasswordHash = localMasterPasswordHash;
this.localMasterPasswordHash = localHashedPassword;
this.code = code;
this.codeVerifier = codeVerifier;
this.ssoRedirectUrl = redirectUrl;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.key = this.setCryptoKeys ? key : null; this.key = this.setCryptoKeys ? key : null;
this.twoFactorService.setProviders(twoFactorProviders); this.twoFactorService.setProviders(twoFactorProviders);
} }
private clearState(): void { private clearState(): void {
this.savedTokenRequest = null;
this.key = null; this.key = null;
this.email = null;
this.masterPasswordHash = null;
this.localMasterPasswordHash = null; this.localMasterPasswordHash = null;
this.code = null;
this.codeVerifier = null;
this.ssoRedirectUrl = null;
this.clientId = null;
this.clientSecret = null;
this.twoFactorService.clearProviders(); this.twoFactorService.clearProviders();
this.twoFactorService.clearSelectedProvider(); this.twoFactorService.clearSelectedProvider();
} }

View File

@@ -26,6 +26,8 @@ import { IdentityTokenResponse } from "jslib-common/models/response/identityToke
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service"; import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
import { HashPurpose } from "jslib-common/enums/hashPurpose"; import { HashPurpose } from "jslib-common/enums/hashPurpose";
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"; import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
import { PasswordTokenRequest } from "jslib-common/models/request/identityToken/passwordTokenRequest";
import { DeviceRequest } from "jslib-common/models/request/deviceRequest";
describe("Cipher Service", () => { describe("Cipher Service", () => {
let cryptoService: SubstituteOf<CryptoService>; let cryptoService: SubstituteOf<CryptoService>;
@@ -327,9 +329,9 @@ describe("Cipher Service", () => {
it("logIn: sends stored 2FA token to server", async () => { it("logIn: sends stored 2FA token to server", async () => {
commonSetup(); commonSetup();
logInSetup(); logInSetup();
tokenService.getTwoFactorToken(email).resolves(twoFactorToken); tokenService.getTwoFactorToken(email).resolves(twoFactorToken);
await authService.logIn(email, masterPassword); await authService.logIn(email, masterPassword);
@@ -351,9 +353,13 @@ describe("Cipher Service", () => {
it("logIn: sends 2FA token entered by user to server", async () => { it("logIn: sends 2FA token entered by user to server", async () => {
commonSetup(); commonSetup();
logInSetup(); logInSetup();
await authService.logIn(email, masterPassword, { provider: twoFactorProviderType, token: twoFactorToken, remember: twoFactorRemember }); await authService.logIn(email, masterPassword, {
provider: twoFactorProviderType,
token: twoFactorToken,
remember: twoFactorRemember,
});
apiService.received(1).postIdentityToken( apiService.received(1).postIdentityToken(
Arg.is((actual) => { Arg.is((actual) => {
@@ -371,15 +377,22 @@ describe("Cipher Service", () => {
); );
}); });
it("logInTwoFactor: sends 2FA token to server when using Master Password", async () => { it("logInTwoFactor: sends 2FA token to server when using Master Password", async () => {
commonSetup(); commonSetup();
authService.email = email; const tokenRequest = new PasswordTokenRequest(email, hashedPassword, null, null, {
authService.masterPasswordHash = hashedPassword; identifier: deviceId,
authService.localMasterPasswordHash = localHashedPassword; } as DeviceRequest);
await authService.logInTwoFactor({ provider: twoFactorProviderType, token: twoFactorToken, remember: twoFactorRemember }); (authService as any).localMasterPasswordHash = localHashedPassword;
(authService as any).email = email;
(authService as any).savedTokenRequest = tokenRequest;
await authService.logInTwoFactor({
provider: twoFactorProviderType,
token: twoFactorToken,
remember: twoFactorRemember,
});
apiService.received(1).postIdentityToken( apiService.received(1).postIdentityToken(
Arg.is((actual) => { Arg.is((actual) => {