1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 02:03:39 +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

@@ -0,0 +1,71 @@
import { Component } from "@angular/core";
import { TestBed } from "@angular/core/testing";
import { Router } from "@angular/router";
import { RouterTestingModule } from "@angular/router/testing";
import { MockProxy, mock } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { activeAuthGuard } from "./active-auth.guard";
@Component({ template: "" })
class EmptyComponent {}
describe("activeAuthGuard", () => {
const setup = (authType: AuthenticationType | null) => {
const loginStrategyService: MockProxy<LoginStrategyServiceAbstraction> =
mock<LoginStrategyServiceAbstraction>();
const currentAuthTypeSubject = new BehaviorSubject<AuthenticationType | null>(authType);
loginStrategyService.currentAuthType$ = currentAuthTypeSubject;
const logService: MockProxy<LogService> = mock<LogService>();
const testBed = TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([
{ path: "", component: EmptyComponent },
{
path: "protected-route",
component: EmptyComponent,
canActivate: [activeAuthGuard()],
},
{ path: "login", component: EmptyComponent },
]),
],
providers: [
{ provide: LoginStrategyServiceAbstraction, useValue: loginStrategyService },
{ provide: LogService, useValue: logService },
],
declarations: [EmptyComponent],
});
return {
router: testBed.inject(Router),
logService,
loginStrategyService,
};
};
it("creates the guard", () => {
const { router } = setup(AuthenticationType.Password);
expect(router).toBeTruthy();
});
it("allows access with an active login session", async () => {
const { router } = setup(AuthenticationType.Password);
await router.navigate(["protected-route"]);
expect(router.url).toBe("/protected-route");
});
it("redirects to login with no active session", async () => {
const { router, logService } = setup(null);
await router.navigate(["protected-route"]);
expect(router.url).toBe("/login");
expect(logService.error).toHaveBeenCalledWith("No active login session found.");
});
});

View File

@@ -0,0 +1,28 @@
import { inject } from "@angular/core";
import { CanActivateFn, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
/**
* Guard that ensures there is an active login session before allowing access
* to the new device verification route.
* If not, redirects to login.
*/
export function activeAuthGuard(): CanActivateFn {
return async () => {
const loginStrategyService = inject(LoginStrategyServiceAbstraction);
const logService = inject(LogService);
const router = inject(Router);
// Check if we have a valid login session
const authType = await firstValueFrom(loginStrategyService.currentAuthType$);
if (authType === null) {
logService.error("No active login session found.");
return router.createUrlTree(["/login"]);
}
return true;
};
}

View File

@@ -1,4 +1,5 @@
export * from "./auth.guard";
export * from "./active-auth.guard";
export * from "./lock.guard";
export * from "./redirect.guard";
export * from "./tde-decryption-required.guard";