1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-10 05:30:01 +00:00

Innovation/opaque grant validator (#13918)

* Add grant validator

* Fix 2fa

* Set active endpoint
This commit is contained in:
Bernd Schoolmann
2025-03-20 15:13:02 +01:00
committed by GitHub
parent b6c2eb7d82
commit c84be3eb22
10 changed files with 72 additions and 12 deletions

View File

@@ -11,7 +11,7 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv
import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { PasswordRequest } from "@bitwarden/common/auth/models/request/password.request";
import { CipherConfiguration } from "@bitwarden/common/auth/opaque/models/cipher-configuration";
import { OpaqueCipherConfiguration } from "@bitwarden/common/auth/opaque/models/opaque-cipher-configuration";
import { OpaqueKeyExchangeService } from "@bitwarden/common/auth/opaque/opaque-key-exchange.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
@@ -229,7 +229,7 @@ export class ChangePasswordComponent
});
} else {
const userConfiguredKdf = await this.kdfConfigService.getKdfConfig();
const cipherConfig = CipherConfiguration.fromKdfConfig(
const cipherConfig = OpaqueCipherConfiguration.fromKdfConfig(
userConfiguredKdf.kdfType === KdfType.Argon2id
? userConfiguredKdf
: DEFAULT_OPAQUE_KDF_CONFIG,

View File

@@ -8,13 +8,13 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
import { OpaqueTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/opaque-token.request";
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
import { IdentityCaptchaResponse } from "@bitwarden/common/auth/models/response/identity-captcha.response";
import { IdentityDeviceVerificationResponse } from "@bitwarden/common/auth/models/response/identity-device-verification.response";
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
import { OpaqueCipherConfiguration } from "@bitwarden/common/auth/opaque/models/opaque-cipher-configuration";
import { OpaqueKeyExchangeService } from "@bitwarden/common/auth/opaque/opaque-key-exchange.service";
import { HashPurpose } from "@bitwarden/common/platform/enums";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
@@ -49,7 +49,7 @@ export class OpaqueLoginStrategyData implements LoginStrategyData {
static fromJSON(obj: Jsonify<OpaqueLoginStrategyData>): OpaqueLoginStrategyData {
const data = Object.assign(new OpaqueLoginStrategyData(), obj, {
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
tokenRequest: OpaqueTokenRequest.fromJSON(obj.tokenRequest),
masterKey: SymmetricCryptoKey.fromJSON(obj.masterKey),
});
return data;
@@ -76,6 +76,7 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy {
data: OpaqueLoginStrategyData,
private passwordStrengthService: PasswordStrengthServiceAbstraction,
private policyService: PolicyService,
private opaqueKeyExchangeService: OpaqueKeyExchangeService,
...sharedDeps: ConstructorParameters<typeof BaseLoginStrategy>
) {
super(...sharedDeps);
@@ -87,8 +88,15 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy {
}
override async logIn(credentials: OpaqueLoginCredentials) {
this.logService.info("Logging in with OPAQUE");
const { email, masterPassword, kdfConfig, cipherConfiguration, twoFactor } = credentials;
const { sessionId } = await this.opaqueKeyExchangeService.login(
email,
masterPassword,
OpaqueCipherConfiguration.fromAny(cipherConfiguration),
);
const data = new OpaqueLoginStrategyData();
data.userEnteredEmail = email;
@@ -109,6 +117,7 @@ export class OpaqueLoginStrategy extends BaseLoginStrategy {
data.tokenRequest = new OpaqueTokenRequest(
email,
await this.buildTwoFactor(twoFactor, email),
sessionId,
await this.buildDeviceRequest(),
);

View File

@@ -303,7 +303,12 @@ export class PasswordLoginStrategy extends BaseLoginStrategy {
);
try {
await this.opaqueKeyExchangeService.register(masterPassword, userKey, cipherConfig);
const sessionId = await this.opaqueKeyExchangeService.register(
masterPassword,
userKey,
cipherConfig,
);
await this.opaqueKeyExchangeService.setRegistrationActive(sessionId);
} catch (error) {
// If this process fails for any reason, we don't want to stop the login process
// so just log the error and continue.

View File

@@ -433,6 +433,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
data?.opaque ?? new OpaqueLoginStrategyData(),
this.passwordStrengthService,
this.policyService,
this.opaqueKeyExchangeService,
...sharedDeps,
);
default:

View File

@@ -11,6 +11,7 @@ export class OpaqueTokenRequest extends TokenRequest {
constructor(
public email: string,
protected twoFactor: TokenTwoFactorRequest,
public sessionId: string,
device?: DeviceRequest,
public newDeviceOtp?: string,
) {
@@ -21,8 +22,9 @@ export class OpaqueTokenRequest extends TokenRequest {
const obj = super.toIdentityToken(clientId);
// TODO: what grant type for OPAQUE?
obj.grant_type = "password";
obj.grant_type = "opaque-ke";
obj.username = this.email;
obj.sessionId = this.sessionId;
if (this.newDeviceOtp) {
obj.newDeviceOtp = this.newDeviceOtp;

View File

@@ -13,6 +13,7 @@ import { LoginStartRequest } from "./models/login-start.request";
import { OpaqueCipherConfiguration } from "./models/opaque-cipher-configuration";
import { RegistrationFinishRequest } from "./models/registration-finish.request";
import { RegistrationStartRequest } from "./models/registration-start.request";
import { SetRegistrationActiveRequest } from "./models/set-registration-active.request";
import { OpaqueKeyExchangeApiService } from "./opaque-key-exchange-api.service";
import { OpaqueKeyExchangeService } from "./opaque-key-exchange.service";
@@ -74,6 +75,12 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService
return registrationStartResponse.sessionId;
}
async setRegistrationActive(sessionId: OpaqueSessionId): Promise<void> {
await this.opaqueKeyExchangeApiService.setRegistrationActive(
new SetRegistrationActiveRequest(sessionId),
);
}
// TODO: we will likely have to break this apart to return the start / finish requests
// so that the opaque login strategy can send both to the identity token endpoint
// in separate calls.
@@ -81,7 +88,7 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService
email: string,
masterPassword: string,
cipherConfig: OpaqueCipherConfiguration,
): Promise<Uint8Array> {
): Promise<{ sessionId: string; exportKey: Uint8Array }> {
if (!email || !masterPassword || !cipherConfig) {
throw new Error(
`Unable to log in user with missing parameters. email exists: ${!!email}; masterPassword exists: ${!!masterPassword}; cipherConfig exists: ${!!cipherConfig}`,
@@ -112,6 +119,6 @@ export class DefaultOpaqueKeyExchangeService implements OpaqueKeyExchangeService
throw new Error("Login failed");
}
return loginFinish.export_key;
return { sessionId: loginStartResponse.sessionId, exportKey: loginFinish.export_key };
}
}

View File

@@ -17,6 +17,16 @@ export class OpaqueCipherConfiguration {
this.argon2Parameters = ksf;
}
static fromAny(config: any): OpaqueCipherConfiguration {
if (
config.cipherSuite !==
"OPAQUE_3_RISTRETTO255_OPRF_RISTRETTO255_KEGROUP_3DH_KEX_ARGON2ID13_KSF"
) {
throw new Error("Unsupported cipher suite");
}
return new OpaqueCipherConfiguration(config.argon2Parameters);
}
/**
* Converts from Bitwarden KDF configs to OPAQUE KSF configs.
*

View File

@@ -0,0 +1,5 @@
import { OpaqueSessionId } from "@bitwarden/common/types/guid";
export class SetRegistrationActiveRequest {
constructor(readonly sessionId: OpaqueSessionId) {}
}

View File

@@ -10,6 +10,7 @@ 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 { SetRegistrationActiveRequest } from "./models/set-registration-active.request";
export class OpaqueKeyExchangeApiService {
constructor(
@@ -45,13 +46,25 @@ export class OpaqueKeyExchangeApiService {
return new RegistrationFinishResponse(response);
}
async setRegistrationActive(request: SetRegistrationActiveRequest): Promise<void> {
const env = await firstValueFrom(this.environmentService.environment$);
await this.apiService.send(
"POST",
`/opaque/set-registration-active`,
request,
true,
true,
env.getApiUrl(),
);
}
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,
false,
true,
env.getApiUrl(),
);
@@ -64,10 +77,10 @@ export class OpaqueKeyExchangeApiService {
"POST",
`/opaque/finish-login`,
request,
true,
false,
true,
env.getApiUrl(),
);
return response.success;
return response == true;
}
}

View File

@@ -14,6 +14,11 @@ export abstract class OpaqueKeyExchangeService {
cipherConfiguration: OpaqueCipherConfiguration,
): Promise<OpaqueSessionId>;
/**
* Set the registration as the active authentication method for the user.
*/
abstract setRegistrationActive(sessionId: OpaqueSessionId): Promise<void>;
/**
* Authenticate using the Opaque login method. Returns the export key, which must be used
* in combination with the rotateable keyset returned from the token endpoint.
@@ -23,5 +28,8 @@ export abstract class OpaqueKeyExchangeService {
email: string,
masterPassword: string,
cipherConfiguration: OpaqueCipherConfiguration,
): Promise<Uint8Array>;
): Promise<{
sessionId: string;
exportKey: Uint8Array;
}>;
}