1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 08:13:42 +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:
Alec Rippberger
2025-01-23 12:57:48 -06:00
committed by GitHub
parent f50f5ef70b
commit aa1c0ca0ee
35 changed files with 852 additions and 86 deletions

View File

@@ -10,7 +10,7 @@ import { ButtonModule } from "@bitwarden/components";
* It provides a button to navigate to the login page.
*/
@Component({
selector: "app-two-factor-expired",
selector: "app-authentication-timeout",
standalone: true,
imports: [CommonModule, JslibModule, ButtonModule, RouterModule],
template: `
@@ -22,4 +22,4 @@ import { ButtonModule } from "@bitwarden/components";
</a>
`,
})
export class TwoFactorTimeoutComponent {}
export class AuthenticationTimeoutComponent {}

View File

@@ -86,12 +86,12 @@ describe("TwoFactorComponent", () => {
};
let selectedUserDecryptionOptions: BehaviorSubject<UserDecryptionOptions>;
let twoFactorTimeoutSubject: BehaviorSubject<boolean>;
let authenticationSessionTimeoutSubject: BehaviorSubject<boolean>;
beforeEach(() => {
twoFactorTimeoutSubject = new BehaviorSubject<boolean>(false);
authenticationSessionTimeoutSubject = new BehaviorSubject<boolean>(false);
mockLoginStrategyService = mock<LoginStrategyServiceAbstraction>();
mockLoginStrategyService.twoFactorTimeout$ = twoFactorTimeoutSubject;
mockLoginStrategyService.authenticationSessionTimeout$ = authenticationSessionTimeoutSubject;
mockRouter = mock<Router>();
mockI18nService = mock<I18nService>();
mockApiService = mock<ApiService>();
@@ -153,7 +153,9 @@ describe("TwoFactorComponent", () => {
}),
};
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(null);
selectedUserDecryptionOptions = new BehaviorSubject<UserDecryptionOptions>(
mockUserDecryptionOpts.withMasterPassword,
);
mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions;
TestBed.configureTestingModule({
@@ -497,8 +499,8 @@ describe("TwoFactorComponent", () => {
});
it("navigates to the timeout route when timeout expires", async () => {
twoFactorTimeoutSubject.next(true);
authenticationSessionTimeoutSubject.next(true);
expect(mockRouter.navigate).toHaveBeenCalledWith(["2fa-timeout"]);
expect(mockRouter.navigate).toHaveBeenCalledWith(["authentication-timeout"]);
});
});

View File

@@ -71,7 +71,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
protected changePasswordRoute = "set-password";
protected forcePasswordResetRoute = "update-temp-password";
protected successRoute = "vault";
protected twoFactorTimeoutRoute = "2fa-timeout";
protected twoFactorTimeoutRoute = "authentication-timeout";
get isDuoProvider(): boolean {
return (
@@ -104,8 +104,8 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
super(environmentService, i18nService, platformUtilsService, toastService);
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
// Add subscription to twoFactorTimeout$ and navigate to twoFactorTimeoutRoute if expired
this.loginStrategyService.twoFactorTimeout$
// Add subscription to authenticationSessionTimeout$ and navigate to twoFactorTimeoutRoute if expired
this.loginStrategyService.authenticationSessionTimeout$
.pipe(takeUntilDestroyed())
.subscribe(async (expired) => {
if (!expired) {