mirror of
https://github.com/bitwarden/browser
synced 2026-02-24 16:43:27 +00:00
PM-19061 - Innovation Sprint - add OPAQUE Login Strategy (#13832)
* ChangePassword - add TODOs to clean up code * LoginComp - Add TODOs for identifying the login strategy ahead of time. * DefaultOpaqueService - Add TODOs * PasswordLoginStrategy - add TODO for renaming * WIP first draft of opaque login strategy * Per discussion with platform, we don't need an abstraction for api services so clean that up. * Extract pre-login method into own service from ApiService + move request model to auth * LoginStrategyService - add todo for adding support for opaque login strategy * PreLoginApiService - add renaming todo * LoginComp + PasswordLoginCredentials - (1) Start integrating pre-login logic into login comp (2) update PasswordLoginCredentials to include kdfConfig to pass into login strat * LoginStrategyServiceAbstraction - login - add OpaqueLoginCredentials * CLI - add todos * LoginComp - add TODO * Add createKdfConfig factory function * LoginStrategyService: switch out to more specific password strategy * Fix type errors * Add jsdoc * Revert / remove TODOs and old draft work * add missing dep * PreLoginResponse - Adjust KM import * PreLogin renamed to PrePasswordLogin * Renames + some login strategy service test updates * LoginComp - remove unused import * KdfConfig - Rename validateKdfConfigForPrelogin to validateKdfConfigForPreLogin * LoginStrategyService - (1) Rename makePreloginKey to makePrePasswordLoginMasterKey (2) Refactor makePrePasswordLoginMasterKey to accept an optional KdfConfig so we can keep the logic tested on the LoginStrategyService * LoginStrategyService - add TODOs * Fix non-sdk build errors --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
@@ -4,4 +4,6 @@ export enum AuthenticationType {
|
||||
UserApiKey = 2,
|
||||
AuthRequest = 3,
|
||||
WebAuthn = 4,
|
||||
PasswordHash = 5,
|
||||
Opaque = 6,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ClientType } from "../../../../enums";
|
||||
import { Utils } from "../../../../platform/misc/utils";
|
||||
|
||||
import { DeviceRequest } from "./device.request";
|
||||
import { TokenTwoFactorRequest } from "./token-two-factor.request";
|
||||
import { TokenRequest } from "./token.request";
|
||||
|
||||
// TODO: we might have to support both login start and login finish requests within this?
|
||||
export class OpaqueTokenRequest extends TokenRequest {
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPasswordHash: string,
|
||||
protected twoFactor: TokenTwoFactorRequest,
|
||||
device?: DeviceRequest,
|
||||
public newDeviceOtp?: string,
|
||||
) {
|
||||
super(twoFactor, device);
|
||||
}
|
||||
|
||||
toIdentityToken(clientId: ClientType) {
|
||||
const obj = super.toIdentityToken(clientId);
|
||||
|
||||
obj.grant_type = "password";
|
||||
obj.username = this.email;
|
||||
obj.password = this.masterPasswordHash;
|
||||
|
||||
if (this.newDeviceOtp) {
|
||||
obj.newDeviceOtp = this.newDeviceOtp;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
|
||||
}
|
||||
|
||||
static fromJSON(json: any) {
|
||||
return Object.assign(Object.create(OpaqueTokenRequest.prototype), json, {
|
||||
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||
twoFactor: json.twoFactor
|
||||
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class PrePasswordLoginRequest {
|
||||
email: string;
|
||||
|
||||
constructor(email: string) {
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { KdfType } from "@bitwarden/key-management";
|
||||
import { KdfType, createKdfConfig } from "@bitwarden/key-management";
|
||||
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CipherConfiguration } from "../../opaque/models/cipher-configuration";
|
||||
|
||||
export class PreloginResponse extends BaseResponse {
|
||||
export class PrePasswordLoginResponse extends BaseResponse {
|
||||
kdf: KdfType;
|
||||
kdfIterations: number;
|
||||
kdfMemory?: number;
|
||||
@@ -19,4 +19,8 @@ export class PreloginResponse extends BaseResponse {
|
||||
this.kdfParallelism = this.getResponseProperty("KdfParallelism");
|
||||
this.opaqueConfiguration = this.getResponseProperty("OpaqueConfiguration");
|
||||
}
|
||||
|
||||
toKdfConfig() {
|
||||
return createKdfConfig(this);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
import { LoginFinishRequest } from "./models/login-finish.request";
|
||||
import { LoginStartRequest } from "./models/login-start.request";
|
||||
import { LoginStartResponse } from "./models/login-start.response";
|
||||
import { RegistrationFinishRequest } from "./models/registration-finish.request";
|
||||
import { RegistrationFinishResponse } from "./models/registration-finish.response";
|
||||
import { RegistrationStartRequest } from "./models/registration-start.request";
|
||||
import { RegistrationStartResponse } from "./models/registration-start.response";
|
||||
import { OpaqueApiService } from "./opaque-api.service";
|
||||
|
||||
export class DefaultOpaqueApiService implements OpaqueApiService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async registrationStart(request: RegistrationStartRequest): Promise<RegistrationStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationStartResponse(response);
|
||||
}
|
||||
|
||||
async registrationFinish(
|
||||
request: RegistrationFinishRequest,
|
||||
): Promise<RegistrationFinishResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationFinishResponse(response);
|
||||
}
|
||||
|
||||
async loginStart(request: LoginStartRequest): Promise<LoginStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new LoginStartResponse(response);
|
||||
}
|
||||
|
||||
async loginFinish(request: LoginFinishRequest): Promise<boolean> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return response.success;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,8 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
import { LoginFinishRequest } from "./models/login-finish.request";
|
||||
import { LoginStartRequest } from "./models/login-start.request";
|
||||
import { LoginStartResponse } from "./models/login-start.response";
|
||||
@@ -6,11 +11,63 @@ import { RegistrationFinishResponse } from "./models/registration-finish.respons
|
||||
import { RegistrationStartRequest } from "./models/registration-start.request";
|
||||
import { RegistrationStartResponse } from "./models/registration-start.response";
|
||||
|
||||
export abstract class OpaqueApiService {
|
||||
abstract registrationStart(request: RegistrationStartRequest): Promise<RegistrationStartResponse>;
|
||||
abstract registrationFinish(
|
||||
export class OpaqueApiService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async registrationStart(request: RegistrationStartRequest): Promise<RegistrationStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationStartResponse(response);
|
||||
}
|
||||
|
||||
async registrationFinish(
|
||||
request: RegistrationFinishRequest,
|
||||
): Promise<RegistrationFinishResponse>;
|
||||
abstract loginStart(request: LoginStartRequest): Promise<LoginStartResponse>;
|
||||
abstract loginFinish(request: LoginFinishRequest): Promise<boolean>;
|
||||
): Promise<RegistrationFinishResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-registration`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new RegistrationFinishResponse(response);
|
||||
}
|
||||
|
||||
async loginStart(request: LoginStartRequest): Promise<LoginStartResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/start-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return new LoginStartResponse(response);
|
||||
}
|
||||
|
||||
async loginFinish(request: LoginFinishRequest): Promise<boolean> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const response = await this.apiService.send(
|
||||
"POST",
|
||||
`/opaque/finish-login`,
|
||||
request,
|
||||
true,
|
||||
true,
|
||||
env.getApiUrl(),
|
||||
);
|
||||
return response.success;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||
|
||||
import { PrePasswordLoginRequest } from "../models/request/pre-password-login.request";
|
||||
import { PrePasswordLoginResponse } from "../models/response/pre-password-login.response";
|
||||
|
||||
/**
|
||||
* An API service which facilitates retrieving key derivation information
|
||||
* required for password-based login before the user has authenticated.
|
||||
*/
|
||||
export class PrePasswordLoginApiService {
|
||||
constructor(
|
||||
private apiService: ApiService,
|
||||
private environmentService: EnvironmentService,
|
||||
) {}
|
||||
|
||||
async postPrePasswordLogin(request: PrePasswordLoginRequest): Promise<PrePasswordLoginResponse> {
|
||||
const env = await firstValueFrom(this.environmentService.environment$);
|
||||
const r = await this.apiService.send(
|
||||
"POST",
|
||||
"/accounts/prelogin",
|
||||
request,
|
||||
false,
|
||||
true,
|
||||
env.getIdentityUrl(),
|
||||
);
|
||||
return new PrePasswordLoginResponse(r);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user