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:
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -433,6 +433,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||
data?.opaque ?? new OpaqueLoginStrategyData(),
|
||||
this.passwordStrengthService,
|
||||
this.policyService,
|
||||
this.opaqueKeyExchangeService,
|
||||
...sharedDeps,
|
||||
);
|
||||
default:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { OpaqueSessionId } from "@bitwarden/common/types/guid";
|
||||
|
||||
export class SetRegistrationActiveRequest {
|
||||
constructor(readonly sessionId: OpaqueSessionId) {}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user