1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 22:03:36 +00:00

[PS-74] Fix user authentication state checks (#721)

* Create authService.authStatus, refactor isLocked checks

* Rename authStatus -> getAuthStatus
This commit is contained in:
Thomas Rittson
2022-04-29 21:33:38 +10:00
committed by GitHub
parent d7e554653a
commit 2e2849b4de
10 changed files with 77 additions and 73 deletions

View File

@@ -1,30 +1,29 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router";
import { AuthService } from "jslib-common/abstractions/auth.service";
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service"; import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
import { MessagingService } from "jslib-common/abstractions/messaging.service"; import { MessagingService } from "jslib-common/abstractions/messaging.service";
import { StateService } from "jslib-common/abstractions/state.service"; import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service";
@Injectable() @Injectable()
export class AuthGuardService implements CanActivate { export class AuthGuardService implements CanActivate {
constructor( constructor(
private vaultTimeoutService: VaultTimeoutService, private authService: AuthService,
private router: Router, private router: Router,
private messagingService: MessagingService, private messagingService: MessagingService,
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService
private stateService: StateService
) {} ) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) { async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
const isAuthed = await this.stateService.getIsAuthenticated(); const authStatus = await this.authService.getAuthStatus();
if (!isAuthed) {
if (authStatus === AuthenticationStatus.LoggedOut) {
this.messagingService.send("authBlocked", { url: routerState.url }); this.messagingService.send("authBlocked", { url: routerState.url });
return false; return false;
} }
const locked = await this.vaultTimeoutService.isLocked(); if (authStatus === AuthenticationStatus.Locked) {
if (locked) {
if (routerState != null) { if (routerState != null) {
this.messagingService.send("lockedUrl", { url: routerState.url }); this.messagingService.send("lockedUrl", { url: routerState.url });
} }

View File

@@ -303,6 +303,7 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
PolicyServiceAbstraction, PolicyServiceAbstraction,
KeyConnectorServiceAbstraction, KeyConnectorServiceAbstraction,
StateServiceAbstraction, StateServiceAbstraction,
AuthServiceAbstraction,
LOCKED_CALLBACK, LOCKED_CALLBACK,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
], ],
@@ -346,11 +347,11 @@ export const SYSTEM_LANGUAGE = new InjectionToken<string>("SYSTEM_LANGUAGE");
SyncServiceAbstraction, SyncServiceAbstraction,
AppIdServiceAbstraction, AppIdServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
VaultTimeoutServiceAbstraction,
EnvironmentServiceAbstraction, EnvironmentServiceAbstraction,
LOGOUT_CALLBACK, LOGOUT_CALLBACK,
LogService, LogService,
StateServiceAbstraction, StateServiceAbstraction,
AuthServiceAbstraction,
], ],
}, },
{ {

View File

@@ -1,27 +1,24 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { CanActivate, Router } from "@angular/router"; import { CanActivate, Router } from "@angular/router";
import { StateService } from "jslib-common/abstractions/state.service"; import { AuthService } from "jslib-common/abstractions/auth.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable() @Injectable()
export class LockGuardService implements CanActivate { export class LockGuardService implements CanActivate {
protected homepage = "vault"; protected homepage = "vault";
protected loginpage = "login"; protected loginpage = "login";
constructor( constructor(private authService: AuthService, private router: Router) {}
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService
) {}
async canActivate() { async canActivate() {
if (await this.vaultTimeoutService.isLocked()) { const authStatus = await this.authService.getAuthStatus();
if (authStatus === AuthenticationStatus.Locked) {
return true; return true;
} }
const redirectUrl = (await this.stateService.getIsAuthenticated()) const redirectUrl =
? [this.homepage] authStatus === AuthenticationStatus.LoggedOut ? [this.loginpage] : [this.homepage];
: [this.loginpage];
this.router.navigate(redirectUrl); this.router.navigate(redirectUrl);
return false; return false;

View File

@@ -1,27 +1,25 @@
import { Injectable } from "@angular/core"; import { Injectable } from "@angular/core";
import { CanActivate, Router } from "@angular/router"; import { CanActivate, Router } from "@angular/router";
import { StateService } from "jslib-common/abstractions/state.service"; import { AuthService } from "jslib-common/abstractions/auth.service";
import { VaultTimeoutService } from "jslib-common/abstractions/vaultTimeout.service"; import { AuthenticationStatus } from "jslib-common/enums/authenticationStatus";
@Injectable() @Injectable()
export class UnauthGuardService implements CanActivate { export class UnauthGuardService implements CanActivate {
protected homepage = "vault"; protected homepage = "vault";
constructor( constructor(private authService: AuthService, private router: Router) {}
private vaultTimeoutService: VaultTimeoutService,
private router: Router,
private stateService: StateService
) {}
async canActivate() { async canActivate() {
const isAuthed = await this.stateService.getIsAuthenticated(); const authStatus = await this.authService.getAuthStatus();
if (isAuthed) {
const locked = await this.vaultTimeoutService.isLocked(); if (authStatus === AuthenticationStatus.LoggedOut) {
if (locked) { return true;
return this.router.createUrlTree(["lock"]);
}
return this.router.createUrlTree([this.homepage]);
} }
return true;
if (authStatus === AuthenticationStatus.Locked) {
return this.router.createUrlTree(["lock"]);
}
return this.router.createUrlTree([this.homepage]);
} }
} }

View File

@@ -1,3 +1,4 @@
import { AuthenticationStatus } from "../enums/authenticationStatus";
import { AuthResult } from "../models/domain/authResult"; import { AuthResult } from "../models/domain/authResult";
import { import {
ApiLogInCredentials, ApiLogInCredentials,
@@ -22,4 +23,5 @@ export abstract class AuthService {
authingWithApiKey: () => boolean; authingWithApiKey: () => boolean;
authingWithSso: () => boolean; authingWithSso: () => boolean;
authingWithPassword: () => boolean; authingWithPassword: () => boolean;
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
} }

View File

@@ -1,5 +1,4 @@
export abstract class VaultTimeoutService { export abstract class VaultTimeoutService {
isLocked: (userId?: string) => Promise<boolean>;
checkVaultTimeout: () => Promise<void>; checkVaultTimeout: () => Promise<void>;
lock: (allowSoftLock?: boolean, userId?: string) => Promise<void>; lock: (allowSoftLock?: boolean, userId?: string) => Promise<void>;
logOut: (userId?: string) => Promise<void>; logOut: (userId?: string) => Promise<void>;

View File

@@ -1,6 +1,5 @@
export enum AuthenticationStatus { export enum AuthenticationStatus {
Locked = "locked", LoggedOut = 0,
Unlocked = "unlocked", Locked = 1,
LoggedOut = "loggedOut", Unlocked = 2,
Active = "active",
} }

View File

@@ -11,8 +11,10 @@ import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/twoFactor.service"; import { TwoFactorService } from "../abstractions/twoFactor.service";
import { AuthenticationStatus } from "../enums/authenticationStatus";
import { AuthenticationType } from "../enums/authenticationType"; import { AuthenticationType } from "../enums/authenticationType";
import { KdfType } from "../enums/kdfType"; import { KdfType } from "../enums/kdfType";
import { KeySuffixOptions } from "../enums/keySuffixOptions";
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy"; import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy"; import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy"; import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
@@ -157,6 +159,31 @@ export class AuthService implements AuthServiceAbstraction {
return this.logInStrategy instanceof PasswordLogInStrategy; return this.logInStrategy instanceof PasswordLogInStrategy;
} }
async getAuthStatus(userId?: string): Promise<AuthenticationStatus> {
const isAuthenticated = await this.stateService.getIsAuthenticated({ userId: userId });
if (!isAuthenticated) {
return AuthenticationStatus.LoggedOut;
}
// Keys aren't stored for a device that is locked or logged out
// Make sure we're logged in before checking this, otherwise we could mix up those states
const neverLock =
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
// We should refactor here.
await this.cryptoService.getKey(KeySuffixOptions.Auto, userId);
}
const hasKeyInMemory = await this.cryptoService.hasKeyInMemory(userId);
if (!hasKeyInMemory) {
return AuthenticationStatus.Locked;
}
return AuthenticationStatus.Unlocked;
}
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> { async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
email = email.trim().toLowerCase(); email = email.trim().toLowerCase();
let kdf: KdfType = null; let kdf: KdfType = null;

View File

@@ -3,12 +3,13 @@ import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
import { ApiService } from "../abstractions/api.service"; import { ApiService } from "../abstractions/api.service";
import { AppIdService } from "../abstractions/appId.service"; import { AppIdService } from "../abstractions/appId.service";
import { AuthService } from "../abstractions/auth.service";
import { EnvironmentService } from "../abstractions/environment.service"; import { EnvironmentService } from "../abstractions/environment.service";
import { LogService } from "../abstractions/log.service"; import { LogService } from "../abstractions/log.service";
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service"; import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { SyncService } from "../abstractions/sync.service"; import { SyncService } from "../abstractions/sync.service";
import { VaultTimeoutService } from "../abstractions/vaultTimeout.service"; import { AuthenticationStatus } from "../enums/authenticationStatus";
import { NotificationType } from "../enums/notificationType"; import { NotificationType } from "../enums/notificationType";
import { import {
NotificationResponse, NotificationResponse,
@@ -29,11 +30,11 @@ export class NotificationsService implements NotificationsServiceAbstraction {
private syncService: SyncService, private syncService: SyncService,
private appIdService: AppIdService, private appIdService: AppIdService,
private apiService: ApiService, private apiService: ApiService,
private vaultTimeoutService: VaultTimeoutService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private logoutCallback: (expired: boolean) => Promise<void>, private logoutCallback: (expired: boolean) => Promise<void>,
private logService: LogService, private logService: LogService,
private stateService: StateService private stateService: StateService,
private authService: AuthService
) { ) {
this.environmentService.urls.subscribe(() => { this.environmentService.urls.subscribe(() => {
if (!this.inited) { if (!this.inited) {
@@ -216,11 +217,8 @@ export class NotificationsService implements NotificationsServiceAbstraction {
} }
private async isAuthedAndUnlocked() { private async isAuthedAndUnlocked() {
if (await this.stateService.getIsAuthenticated()) { const authStatus = await this.authService.getAuthStatus();
const locked = await this.vaultTimeoutService.isLocked(); return authStatus >= AuthenticationStatus.Unlocked;
return !locked;
}
return false;
} }
private random(min: number, max: number) { private random(min: number, max: number) {

View File

@@ -1,3 +1,4 @@
import { AuthService } from "../abstractions/auth.service";
import { CipherService } from "../abstractions/cipher.service"; import { CipherService } from "../abstractions/cipher.service";
import { CollectionService } from "../abstractions/collection.service"; import { CollectionService } from "../abstractions/collection.service";
import { CryptoService } from "../abstractions/crypto.service"; import { CryptoService } from "../abstractions/crypto.service";
@@ -10,7 +11,7 @@ import { SearchService } from "../abstractions/search.service";
import { StateService } from "../abstractions/state.service"; import { StateService } from "../abstractions/state.service";
import { TokenService } from "../abstractions/token.service"; import { TokenService } from "../abstractions/token.service";
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vaultTimeout.service"; import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "../abstractions/vaultTimeout.service";
import { KeySuffixOptions } from "../enums/keySuffixOptions"; import { AuthenticationStatus } from "../enums/authenticationStatus";
import { PolicyType } from "../enums/policyType"; import { PolicyType } from "../enums/policyType";
export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
@@ -28,6 +29,7 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
private policyService: PolicyService, private policyService: PolicyService,
private keyConnectorService: KeyConnectorService, private keyConnectorService: KeyConnectorService,
private stateService: StateService, private stateService: StateService,
private authService: AuthService,
private lockedCallback: (userId?: string) => Promise<void> = null, private lockedCallback: (userId?: string) => Promise<void> = null,
private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null private loggedOutCallback: (expired: boolean, userId?: string) => Promise<void> = null
) {} ) {}
@@ -48,20 +50,6 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds setInterval(() => this.checkVaultTimeout(), 10 * 1000); // check every 10 seconds
} }
// Keys aren't stored for a device that is locked or logged out.
async isLocked(userId?: string): Promise<boolean> {
const neverLock =
(await this.cryptoService.hasKeyStored(KeySuffixOptions.Auto, userId)) &&
!(await this.stateService.getEverBeenUnlocked({ userId: userId }));
if (neverLock) {
// TODO: This also _sets_ the key so when we check memory in the next line it finds a key.
// We should refactor here.
await this.cryptoService.getKey(KeySuffixOptions.Auto, userId);
}
return !(await this.cryptoService.hasKeyInMemory(userId));
}
async checkVaultTimeout(): Promise<void> { async checkVaultTimeout(): Promise<void> {
if (await this.platformUtilsService.isViewOpen()) { if (await this.platformUtilsService.isViewOpen()) {
return; return;
@@ -187,16 +175,12 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction {
await this.stateService.setProtectedPin(null, { userId: userId }); await this.stateService.setProtectedPin(null, { userId: userId });
} }
private async isLoggedOut(userId?: string): Promise<boolean> {
return !(await this.stateService.getIsAuthenticated({ userId: userId }));
}
private async shouldLock(userId: string): Promise<boolean> { private async shouldLock(userId: string): Promise<boolean> {
if (await this.isLoggedOut(userId)) { const authStatus = await this.authService.getAuthStatus(userId);
return false; if (
} authStatus === AuthenticationStatus.Locked ||
authStatus === AuthenticationStatus.LoggedOut
if (await this.isLocked(userId)) { ) {
return false; return false;
} }