mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 00:03:56 +00:00
feat(auth): [PM-8221] implement device verification for unknown devices
Add device verification flow that requires users to enter an OTP when logging in from an unrecognized device. This includes: - New device verification route and guard - Email OTP verification component - Authentication timeout handling PM-8221
This commit is contained in:
@@ -70,6 +70,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
|
||||
import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
@@ -151,7 +152,12 @@ export abstract class ApiService {
|
||||
| SsoTokenRequest
|
||||
| UserApiTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
|
||||
) => Promise<
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityCaptchaResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
>;
|
||||
refreshIdentityToken: () => Promise<any>;
|
||||
|
||||
getProfile: () => Promise<ProfileResponse>;
|
||||
|
||||
@@ -22,6 +22,7 @@ export class AuthResult {
|
||||
ssoEmail2FaSessionToken?: string;
|
||||
email: string;
|
||||
requiresEncryptionKeyMigration: boolean;
|
||||
requiresDeviceVerification: boolean;
|
||||
|
||||
get requiresCaptcha() {
|
||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||
|
||||
@@ -13,6 +13,7 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
|
||||
public captchaResponse: string,
|
||||
protected twoFactor: TokenTwoFactorRequest,
|
||||
device?: DeviceRequest,
|
||||
public newDeviceOtp?: string,
|
||||
) {
|
||||
super(twoFactor, device);
|
||||
}
|
||||
@@ -28,6 +29,10 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
|
||||
obj.captchaResponse = this.captchaResponse;
|
||||
}
|
||||
|
||||
if (this.newDeviceOtp) {
|
||||
obj.newDeviceOtp = this.newDeviceOtp;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||
|
||||
export class IdentityDeviceVerificationResponse extends BaseResponse {
|
||||
deviceVerified: boolean;
|
||||
captchaToken: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.deviceVerified = this.getResponseProperty("DeviceVerified") ?? false;
|
||||
|
||||
this.captchaToken = this.getResponseProperty("CaptchaBypassToken");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
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";
|
||||
|
||||
export type IdentityResponse =
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityDeviceVerificationResponse;
|
||||
@@ -47,6 +47,7 @@ export enum FeatureFlag {
|
||||
PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form",
|
||||
PrivateKeyRegeneration = "pm-12241-private-key-regeneration",
|
||||
ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs",
|
||||
NewDeviceVerification = "new-device-verification",
|
||||
}
|
||||
|
||||
export type AllowedFeatureFlagTypes = boolean | number | string;
|
||||
@@ -104,6 +105,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.PM9111ExtensionPersistAddEditForm]: FALSE,
|
||||
[FeatureFlag.PrivateKeyRegeneration]: FALSE,
|
||||
[FeatureFlag.ResellerManagedOrgAlert]: FALSE,
|
||||
[FeatureFlag.NewDeviceVerification]: FALSE,
|
||||
} satisfies Record<FeatureFlag, AllowedFeatureFlagTypes>;
|
||||
|
||||
export type DefaultFeatureFlagValueType = typeof DefaultFeatureFlagValue;
|
||||
|
||||
@@ -78,6 +78,7 @@ import { ApiKeyResponse } from "../auth/models/response/api-key.response";
|
||||
import { AuthRequestResponse } from "../auth/models/response/auth-request.response";
|
||||
import { DeviceVerificationResponse } from "../auth/models/response/device-verification.response";
|
||||
import { IdentityCaptchaResponse } from "../auth/models/response/identity-captcha.response";
|
||||
import { IdentityDeviceVerificationResponse } from "../auth/models/response/identity-device-verification.response";
|
||||
import { IdentityTokenResponse } from "../auth/models/response/identity-token.response";
|
||||
import { IdentityTwoFactorResponse } from "../auth/models/response/identity-two-factor.response";
|
||||
import { KeyConnectorUserKeyResponse } from "../auth/models/response/key-connector-user-key.response";
|
||||
@@ -158,6 +159,12 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
private isWebClient = false;
|
||||
private isDesktopClient = false;
|
||||
|
||||
/**
|
||||
* The message (responseJson.ErrorModel.Message) that comes back from the server when a new device verification is required.
|
||||
*/
|
||||
private static readonly NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE =
|
||||
"new device verification required";
|
||||
|
||||
constructor(
|
||||
private tokenService: TokenService,
|
||||
private platformUtilsService: PlatformUtilsService,
|
||||
@@ -197,7 +204,12 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
| PasswordTokenRequest
|
||||
| SsoTokenRequest
|
||||
| WebAuthnLoginTokenRequest,
|
||||
): Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse> {
|
||||
): Promise<
|
||||
| IdentityTokenResponse
|
||||
| IdentityTwoFactorResponse
|
||||
| IdentityCaptchaResponse
|
||||
| IdentityDeviceVerificationResponse
|
||||
> {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
Accept: "application/json",
|
||||
@@ -245,6 +257,11 @@ export class ApiService implements ApiServiceAbstraction {
|
||||
Object.keys(responseJson.HCaptcha_SiteKey).length
|
||||
) {
|
||||
return new IdentityCaptchaResponse(responseJson);
|
||||
} else if (
|
||||
response.status === 400 &&
|
||||
responseJson?.ErrorModel?.Message === ApiService.NEW_DEVICE_VERIFICATION_REQUIRED_MESSAGE
|
||||
) {
|
||||
return new IdentityDeviceVerificationResponse(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user