1
0
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:
Jared Snider
2025-03-17 06:41:46 -04:00
committed by GitHub
parent b2d949dd1c
commit a2ba965abd
35 changed files with 695 additions and 254 deletions

View File

@@ -4,4 +4,6 @@ export enum AuthenticationType {
UserApiKey = 2,
AuthRequest = 3,
WebAuthn = 4,
PasswordHash = 5,
Opaque = 6,
}

View File

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

View File

@@ -0,0 +1,7 @@
export class PrePasswordLoginRequest {
email: string;
constructor(email: string) {
this.email = email;
}
}

View File

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

View File

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

View File

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

View File

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