1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-23 03:33:54 +00:00

Auth/ps 2298 reorg auth (#4564)

* Move auth service factories to Auth team

* Move authentication componenets to Auth team

* Move auth guard services to Auth team

* Move Duo content script to Auth team

* Move auth CLI commands to Auth team

* Move Desktop Account components to Auth Team

* Move Desktop guards to Auth team

* Move two-factor provider images to Auth team

* Move web Accounts components to Auth Team

* Move web settings components to Auth Team

* Move web two factor images to Auth Team

* Fix missed import changes for Auth Team

* Fix Linting errors

* Fix missed CLI imports

* Fix missed Desktop imports

* Revert images move

* Fix missed imports in Web

* Move angular lib components to Auth Team

* Move angular auth guards to Auth team

* Move strategy specs to Auth team

* Update .eslintignore for new paths

* Move lib common abstractions to Auth team

* Move services to Auth team

* Move common lib enums to Auth team

* Move webauthn iframe to Auth team

* Move lib common domain models to Auth team

* Move common lib requests to Auth team

* Move response models to Auth team

* Clean up whitelist

* Move bit web components to Auth team

* Move SSO and SCIM files to Auth team

* Revert move SCIM to Auth team

SCIM belongs to Admin Console team

* Move captcha to Auth team

* Move key connector to Auth team

* Move emergency access to auth team

* Delete extra file

* linter fixes

* Move kdf config to auth team

* Fix whitelist

* Fix duo autoformat

* Complete two factor provider request move

* Fix whitelist names

* Fix login capitalization

* Revert hint dependency reordering

* Revert hint dependency reordering

* Revert hint component

This components is being picked up as a move between clients

* Move web hint component to Auth team

* Move new files to auth team

* Fix desktop build

* Fix browser build
This commit is contained in:
Matt Gibson
2023-02-06 16:53:37 -05:00
committed by GitHub
parent 084c89107e
commit cf972e784c
377 changed files with 1030 additions and 998 deletions

View File

@@ -0,0 +1,26 @@
import { ApiService } from "../../abstractions/api.service";
import { LogService } from "../../abstractions/log.service";
import { UserVerificationService } from "../../abstractions/userVerification/userVerification.service.abstraction";
import { Verification } from "../../types/verification";
import { AccountApiService } from "../abstractions/account-api.service";
import { InternalAccountService } from "../abstractions/account.service";
export class AccountApiServiceImplementation implements AccountApiService {
constructor(
private apiService: ApiService,
private userVerificationService: UserVerificationService,
private logService: LogService,
private accountService: InternalAccountService
) {}
async deleteAccount(verification: Verification): Promise<void> {
try {
const verificationRequest = await this.userVerificationService.buildRequest(verification);
await this.apiService.send("DELETE", "/accounts", verificationRequest, true, false);
this.accountService.delete();
} catch (e) {
this.logService.error(e);
throw e;
}
}
}

View File

@@ -0,0 +1,16 @@
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { InternalAccountService } from "../../auth/abstractions/account.service";
export class AccountServiceImplementation implements InternalAccountService {
constructor(private messagingService: MessagingService, private logService: LogService) {}
async delete(): Promise<void> {
try {
this.messagingService.send("logout");
} catch (e) {
this.logService.error(e);
throw e;
}
}
}

View File

@@ -0,0 +1,334 @@
import { Observable, Subject } from "rxjs";
import { ApiService } from "../../abstractions/api.service";
import { AppIdService } from "../../abstractions/appId.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { EncryptService } from "../../abstractions/encrypt.service";
import { EnvironmentService } from "../../abstractions/environment.service";
import { I18nService } from "../../abstractions/i18n.service";
import { LogService } from "../../abstractions/log.service";
import { MessagingService } from "../../abstractions/messaging.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import { StateService } from "../../abstractions/state.service";
import { KdfType } from "../../enums/kdfType";
import { KeySuffixOptions } from "../../enums/keySuffixOptions";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { PreloginRequest } from "../../models/request/prelogin.request";
import { ErrorResponse } from "../../models/response/error.response";
import { AuthRequestPushNotification } from "../../models/response/notification.response";
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
import { KeyConnectorService } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { TwoFactorService } from "../abstractions/two-factor.service";
import { AuthenticationStatus } from "../enums/authentication-status";
import { AuthenticationType } from "../enums/authentication-type";
import { PasswordLogInStrategy } from "../login-strategies/password-login.strategy";
import { PasswordlessLogInStrategy } from "../login-strategies/passwordless-login.strategy";
import { SsoLogInStrategy } from "../login-strategies/sso-login.strategy";
import { UserApiLogInStrategy } from "../login-strategies/user-api-login.strategy";
import { AuthResult } from "../models/domain/auth-result";
import { KdfConfig } from "../models/domain/kdf-config";
import {
UserApiLogInCredentials,
PasswordLogInCredentials,
SsoLogInCredentials,
PasswordlessLogInCredentials,
} from "../models/domain/log-in-credentials";
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
import { PasswordlessAuthRequest } from "../models/request/passwordless-auth.request";
import { AuthRequestResponse } from "../models/response/auth-request.response";
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
export class AuthService implements AuthServiceAbstraction {
get email(): string {
if (
this.logInStrategy instanceof PasswordLogInStrategy ||
this.logInStrategy instanceof PasswordlessLogInStrategy
) {
return this.logInStrategy.email;
}
return null;
}
get masterPasswordHash(): string {
return this.logInStrategy instanceof PasswordLogInStrategy
? this.logInStrategy.masterPasswordHash
: null;
}
get accessCode(): string {
return this.logInStrategy instanceof PasswordlessLogInStrategy
? this.logInStrategy.accessCode
: null;
}
get authRequestId(): string {
return this.logInStrategy instanceof PasswordlessLogInStrategy
? this.logInStrategy.authRequestId
: null;
}
private logInStrategy:
| UserApiLogInStrategy
| PasswordLogInStrategy
| SsoLogInStrategy
| PasswordlessLogInStrategy;
private sessionTimeout: any;
private pushNotificationSubject = new Subject<string>();
constructor(
protected cryptoService: CryptoService,
protected apiService: ApiService,
protected tokenService: TokenService,
protected appIdService: AppIdService,
protected platformUtilsService: PlatformUtilsService,
protected messagingService: MessagingService,
protected logService: LogService,
protected keyConnectorService: KeyConnectorService,
protected environmentService: EnvironmentService,
protected stateService: StateService,
protected twoFactorService: TwoFactorService,
protected i18nService: I18nService,
protected encryptService: EncryptService
) {}
async logIn(
credentials:
| UserApiLogInCredentials
| PasswordLogInCredentials
| SsoLogInCredentials
| PasswordlessLogInCredentials
): Promise<AuthResult> {
this.clearState();
let strategy:
| UserApiLogInStrategy
| PasswordLogInStrategy
| SsoLogInStrategy
| PasswordlessLogInStrategy;
switch (credentials.type) {
case AuthenticationType.Password:
strategy = new PasswordLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this
);
break;
case AuthenticationType.Sso:
strategy = new SsoLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.keyConnectorService
);
break;
case AuthenticationType.UserApi:
strategy = new UserApiLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this.environmentService,
this.keyConnectorService
);
break;
case AuthenticationType.Passwordless:
strategy = new PasswordlessLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
this
);
break;
}
const result = await strategy.logIn(credentials as any);
if (result?.requiresTwoFactor) {
this.saveState(strategy);
}
return result;
}
async logInTwoFactor(
twoFactor: TokenTwoFactorRequest,
captchaResponse: string
): Promise<AuthResult> {
if (this.logInStrategy == null) {
throw new Error(this.i18nService.t("sessionTimeout"));
}
try {
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
this.clearState();
}
return result;
} catch (e) {
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe
if (!(e instanceof ErrorResponse)) {
this.clearState();
}
throw e;
}
}
logOut(callback: () => void) {
callback();
this.messagingService.send("loggedOut");
}
authingWithUserApiKey(): boolean {
return this.logInStrategy instanceof UserApiLogInStrategy;
}
authingWithSso(): boolean {
return this.logInStrategy instanceof SsoLogInStrategy;
}
authingWithPassword(): boolean {
return this.logInStrategy instanceof PasswordLogInStrategy;
}
authingWithPasswordless(): boolean {
return this.logInStrategy instanceof PasswordlessLogInStrategy;
}
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> {
email = email.trim().toLowerCase();
let kdf: KdfType = null;
let kdfConfig: KdfConfig = null;
try {
const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email));
if (preloginResponse != null) {
kdf = preloginResponse.kdf;
kdfConfig = new KdfConfig(
preloginResponse.kdfIterations,
preloginResponse.kdfMemory,
preloginResponse.kdfParallelism
);
}
} catch (e) {
if (e == null || e.statusCode !== 404) {
throw e;
}
}
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfConfig);
}
async authResponsePushNotifiction(notification: AuthRequestPushNotification): Promise<any> {
this.pushNotificationSubject.next(notification.id);
}
getPushNotifcationObs$(): Observable<any> {
return this.pushNotificationSubject.asObservable();
}
async passwordlessLogin(
id: string,
key: string,
requestApproved: boolean
): Promise<AuthRequestResponse> {
const pubKey = Utils.fromB64ToArray(key);
const encryptedKey = await this.cryptoService.rsaEncrypt(
(
await this.cryptoService.getKey()
).encKey,
pubKey.buffer
);
const encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
pubKey.buffer
);
const request = new PasswordlessAuthRequest(
encryptedKey.encryptedString,
encryptedMasterPassword.encryptedString,
await this.appIdService.getAppId(),
requestApproved
);
return await this.apiService.putAuthRequest(id, request);
}
private saveState(
strategy:
| UserApiLogInStrategy
| PasswordLogInStrategy
| SsoLogInStrategy
| PasswordlessLogInStrategy
) {
this.logInStrategy = strategy;
this.startSessionTimeout();
}
private clearState() {
this.logInStrategy = null;
this.clearSessionTimeout();
}
private startSessionTimeout() {
this.clearSessionTimeout();
this.sessionTimeout = setTimeout(() => this.clearState(), sessionTimeoutLength);
}
private clearSessionTimeout() {
if (this.sessionTimeout != null) {
clearTimeout(this.sessionTimeout);
}
}
}

View File

@@ -0,0 +1,144 @@
import { ApiService } from "../../abstractions/api.service";
import { CryptoService } from "../../abstractions/crypto.service";
import { CryptoFunctionService } from "../../abstractions/cryptoFunction.service";
import { LogService } from "../../abstractions/log.service";
import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction";
import { StateService } from "../../abstractions/state.service";
import { OrganizationUserType } from "../../enums/organizationUserType";
import { Utils } from "../../misc/utils";
import { SymmetricCryptoKey } from "../../models/domain/symmetric-crypto-key";
import { KeysRequest } from "../../models/request/keys.request";
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/key-connector.service";
import { TokenService } from "../abstractions/token.service";
import { KdfConfig } from "../models/domain/kdf-config";
import { KeyConnectorUserKeyRequest } from "../models/request/key-connector-user-key.request";
import { SetKeyConnectorKeyRequest } from "../models/request/set-key-connector-key.request";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
constructor(
private stateService: StateService,
private cryptoService: CryptoService,
private apiService: ApiService,
private tokenService: TokenService,
private logService: LogService,
private organizationService: OrganizationService,
private cryptoFunctionService: CryptoFunctionService,
private logoutCallback: (expired: boolean, userId?: string) => void
) {}
setUsesKeyConnector(usesKeyConnector: boolean) {
return this.stateService.setUsesKeyConnector(usesKeyConnector);
}
async getUsesKeyConnector(): Promise<boolean> {
return await this.stateService.getUsesKeyConnector();
}
async userNeedsMigration() {
const loggedInUsingSso = await this.tokenService.getIsExternal();
const requiredByOrganization = (await this.getManagingOrganization()) != null;
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector());
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
}
async migrateUser() {
const organization = await this.getManagingOrganization();
const key = await this.cryptoService.getKey();
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
try {
await this.apiService.postUserKeyToKeyConnector(
organization.keyConnectorUrl,
keyConnectorRequest
);
} catch (e) {
this.handleKeyConnectorError(e);
}
await this.apiService.postConvertToKeyConnector();
}
async getAndSetKey(url: string) {
try {
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
const k = new SymmetricCryptoKey(keyArr);
await this.cryptoService.setKey(k);
} catch (e) {
this.handleKeyConnectorError(e);
}
}
async getManagingOrganization() {
const orgs = await this.organizationService.getAll();
return orgs.find(
(o) =>
o.keyConnectorEnabled &&
o.type !== OrganizationUserType.Admin &&
o.type !== OrganizationUserType.Owner &&
!o.isProviderUser
);
}
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
const { kdf, kdfIterations, kdfMemory, kdfParallelism, keyConnectorUrl } = tokenResponse;
const password = await this.cryptoFunctionService.randomBytes(64);
const kdfConfig = new KdfConfig(kdfIterations, kdfMemory, kdfParallelism);
const k = await this.cryptoService.makeKey(
Utils.fromBufferToB64(password),
await this.tokenService.getEmail(),
kdf,
kdfConfig
);
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
await this.cryptoService.setKey(k);
const encKey = await this.cryptoService.makeEncKey(k);
await this.cryptoService.setEncKey(encKey[1].encryptedString);
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
try {
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
} catch (e) {
this.handleKeyConnectorError(e);
}
const keys = new KeysRequest(pubKey, privKey.encryptedString);
const setPasswordRequest = new SetKeyConnectorKeyRequest(
encKey[1].encryptedString,
kdf,
kdfConfig,
orgId,
keys
);
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
}
async setConvertAccountRequired(status: boolean) {
await this.stateService.setConvertAccountToKeyConnector(status);
}
async getConvertAccountRequired(): Promise<boolean> {
return await this.stateService.getConvertAccountToKeyConnector();
}
async removeConvertAccountRequired() {
await this.stateService.setConvertAccountToKeyConnector(null);
}
async clear() {
await this.removeConvertAccountRequired();
}
private handleKeyConnectorError(e: any) {
this.logService.error(e);
if (this.logoutCallback != null) {
this.logoutCallback(false);
}
throw new Error("Key Connector error");
}
}

View File

@@ -0,0 +1,35 @@
import { StateService } from "../../abstractions/state.service";
import { LoginService as LoginServiceAbstraction } from "../abstractions/login.service";
export class LoginService implements LoginServiceAbstraction {
private _email: string;
private _rememberEmail: boolean;
constructor(private stateService: StateService) {}
getEmail() {
return this._email;
}
getRememberEmail() {
return this._rememberEmail;
}
setEmail(value: string) {
this._email = value;
}
setRememberEmail(value: boolean) {
this._rememberEmail = value;
}
clearValues() {
this._email = null;
this._rememberEmail = null;
}
async saveEmailSettings() {
await this.stateService.setRememberedEmail(this._rememberEmail ? this._email : null);
this.clearValues();
}
}

View File

@@ -0,0 +1,181 @@
import { StateService } from "../../abstractions/state.service";
import { Utils } from "../../misc/utils";
import { TokenService as TokenServiceAbstraction } from "../abstractions/token.service";
import { IdentityTokenResponse } from "../models/response/identity-token.response";
export class TokenService implements TokenServiceAbstraction {
static decodeToken(token: string): Promise<any> {
if (token == null) {
throw new Error("Token not provided.");
}
const parts = token.split(".");
if (parts.length !== 3) {
throw new Error("JWT must have 3 parts");
}
const decoded = Utils.fromUrlB64ToUtf8(parts[1]);
if (decoded == null) {
throw new Error("Cannot decode the token");
}
const decodedToken = JSON.parse(decoded);
return decodedToken;
}
constructor(private stateService: StateService) {}
async setTokens(
accessToken: string,
refreshToken: string,
clientIdClientSecret: [string, string]
): Promise<any> {
await this.setToken(accessToken);
await this.setRefreshToken(refreshToken);
if (clientIdClientSecret != null) {
await this.setClientId(clientIdClientSecret[0]);
await this.setClientSecret(clientIdClientSecret[1]);
}
}
async setClientId(clientId: string): Promise<any> {
return await this.stateService.setApiKeyClientId(clientId);
}
async getClientId(): Promise<string> {
return await this.stateService.getApiKeyClientId();
}
async setClientSecret(clientSecret: string): Promise<any> {
return await this.stateService.setApiKeyClientSecret(clientSecret);
}
async getClientSecret(): Promise<string> {
return await this.stateService.getApiKeyClientSecret();
}
async setToken(token: string): Promise<void> {
await this.stateService.setAccessToken(token);
}
async getToken(): Promise<string> {
return await this.stateService.getAccessToken();
}
async setRefreshToken(refreshToken: string): Promise<any> {
return await this.stateService.setRefreshToken(refreshToken);
}
async getRefreshToken(): Promise<string> {
return await this.stateService.getRefreshToken();
}
async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise<any> {
return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken);
}
async getTwoFactorToken(): Promise<string> {
return await this.stateService.getTwoFactorToken();
}
async clearTwoFactorToken(): Promise<any> {
return await this.stateService.setTwoFactorToken(null);
}
async clearToken(userId?: string): Promise<any> {
await this.stateService.setAccessToken(null, { userId: userId });
await this.stateService.setRefreshToken(null, { userId: userId });
await this.stateService.setApiKeyClientId(null, { userId: userId });
await this.stateService.setApiKeyClientSecret(null, { userId: userId });
}
// jwthelper methods
// ref https://github.com/auth0/angular-jwt/blob/master/src/angularJwt/services/jwt.js
async decodeToken(token?: string): Promise<any> {
token = token ?? (await this.stateService.getAccessToken());
if (token == null) {
throw new Error("Token not found.");
}
return TokenService.decodeToken(token);
}
async getTokenExpirationDate(): Promise<Date> {
const decoded = await this.decodeToken();
if (typeof decoded.exp === "undefined") {
return null;
}
const d = new Date(0); // The 0 here is the key, which sets the date to the epoch
d.setUTCSeconds(decoded.exp);
return d;
}
async tokenSecondsRemaining(offsetSeconds = 0): Promise<number> {
const d = await this.getTokenExpirationDate();
if (d == null) {
return 0;
}
const msRemaining = d.valueOf() - (new Date().valueOf() + offsetSeconds * 1000);
return Math.round(msRemaining / 1000);
}
async tokenNeedsRefresh(minutes = 5): Promise<boolean> {
const sRemaining = await this.tokenSecondsRemaining();
return sRemaining < 60 * minutes;
}
async getUserId(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.sub === "undefined") {
throw new Error("No user id found");
}
return decoded.sub as string;
}
async getEmail(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.email === "undefined") {
throw new Error("No email found");
}
return decoded.email as string;
}
async getEmailVerified(): Promise<boolean> {
const decoded = await this.decodeToken();
if (typeof decoded.email_verified === "undefined") {
throw new Error("No email verification found");
}
return decoded.email_verified as boolean;
}
async getName(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.name === "undefined") {
return null;
}
return decoded.name as string;
}
async getIssuer(): Promise<string> {
const decoded = await this.decodeToken();
if (typeof decoded.iss === "undefined") {
throw new Error("No issuer found");
}
return decoded.iss as string;
}
async getIsExternal(): Promise<boolean> {
const decoded = await this.decodeToken();
return Array.isArray(decoded.amr) && decoded.amr.includes("external");
}
}

View File

@@ -0,0 +1,186 @@
import { I18nService } from "../../abstractions/i18n.service";
import { PlatformUtilsService } from "../../abstractions/platformUtils.service";
import {
TwoFactorProviderDetails,
TwoFactorService as TwoFactorServiceAbstraction,
} from "../abstractions/two-factor.service";
import { TwoFactorProviderType } from "../enums/two-factor-provider-type";
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
export const TwoFactorProviders: Partial<Record<TwoFactorProviderType, TwoFactorProviderDetails>> =
{
[TwoFactorProviderType.Authenticator]: {
type: TwoFactorProviderType.Authenticator,
name: null as string,
description: null as string,
priority: 1,
sort: 1,
premium: false,
},
[TwoFactorProviderType.Yubikey]: {
type: TwoFactorProviderType.Yubikey,
name: null as string,
description: null as string,
priority: 3,
sort: 2,
premium: true,
},
[TwoFactorProviderType.Duo]: {
type: TwoFactorProviderType.Duo,
name: "Duo",
description: null as string,
priority: 2,
sort: 3,
premium: true,
},
[TwoFactorProviderType.OrganizationDuo]: {
type: TwoFactorProviderType.OrganizationDuo,
name: "Duo (Organization)",
description: null as string,
priority: 10,
sort: 4,
premium: false,
},
[TwoFactorProviderType.Email]: {
type: TwoFactorProviderType.Email,
name: null as string,
description: null as string,
priority: 0,
sort: 6,
premium: false,
},
[TwoFactorProviderType.WebAuthn]: {
type: TwoFactorProviderType.WebAuthn,
name: null as string,
description: null as string,
priority: 4,
sort: 5,
premium: true,
},
};
export class TwoFactorService implements TwoFactorServiceAbstraction {
private twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
private selectedTwoFactorProviderType: TwoFactorProviderType = null;
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
) {}
init() {
TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle");
TwoFactorProviders[TwoFactorProviderType.Email].description = this.i18nService.t("emailDesc");
TwoFactorProviders[TwoFactorProviderType.Authenticator].name =
this.i18nService.t("authenticatorAppTitle");
TwoFactorProviders[TwoFactorProviderType.Authenticator].description =
this.i18nService.t("authenticatorAppDesc");
TwoFactorProviders[TwoFactorProviderType.Duo].description = this.i18nService.t("duoDesc");
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].name =
"Duo (" + this.i18nService.t("organization") + ")";
TwoFactorProviders[TwoFactorProviderType.OrganizationDuo].description =
this.i18nService.t("duoOrganizationDesc");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].name = this.i18nService.t("webAuthnTitle");
TwoFactorProviders[TwoFactorProviderType.WebAuthn].description =
this.i18nService.t("webAuthnDesc");
TwoFactorProviders[TwoFactorProviderType.Yubikey].name = this.i18nService.t("yubiKeyTitle");
TwoFactorProviders[TwoFactorProviderType.Yubikey].description =
this.i18nService.t("yubiKeyDesc");
}
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
const providers: any[] = [];
if (this.twoFactorProvidersData == null) {
return providers;
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.OrganizationDuo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Authenticator)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Yubikey)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]);
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.Duo) &&
this.platformUtilsService.supportsDuo()
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]);
}
if (
this.twoFactorProvidersData.has(TwoFactorProviderType.WebAuthn) &&
this.platformUtilsService.supportsWebAuthn(win)
) {
providers.push(TwoFactorProviders[TwoFactorProviderType.WebAuthn]);
}
if (this.twoFactorProvidersData.has(TwoFactorProviderType.Email)) {
providers.push(TwoFactorProviders[TwoFactorProviderType.Email]);
}
return providers;
}
getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
if (this.twoFactorProvidersData == null) {
return null;
}
if (
this.selectedTwoFactorProviderType != null &&
this.twoFactorProvidersData.has(this.selectedTwoFactorProviderType)
) {
return this.selectedTwoFactorProviderType;
}
let providerType: TwoFactorProviderType = null;
let providerPriority = -1;
this.twoFactorProvidersData.forEach((_value, type) => {
const provider = (TwoFactorProviders as any)[type];
if (provider != null && provider.priority > providerPriority) {
if (type === TwoFactorProviderType.WebAuthn && !webAuthnSupported) {
return;
}
providerType = type;
providerPriority = provider.priority;
}
});
return providerType;
}
setSelectedProvider(type: TwoFactorProviderType) {
this.selectedTwoFactorProviderType = type;
}
clearSelectedProvider() {
this.selectedTwoFactorProviderType = null;
}
setProviders(response: IdentityTwoFactorResponse) {
this.twoFactorProvidersData = response.twoFactorProviders2;
}
clearProviders() {
this.twoFactorProvidersData = null;
}
getProviders() {
return this.twoFactorProvidersData;
}
}

View File

@@ -0,0 +1,14 @@
import { ApiService } from "../../../abstractions/api.service";
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
export class UserVerificationApiService implements UserVerificationApiServiceAbstraction {
constructor(private apiService: ApiService) {}
postAccountVerifyOTP(request: VerifyOTPRequest): Promise<void> {
return this.apiService.send("POST", "/accounts/verify-otp", request, true, false);
}
async postAccountRequestOTP(): Promise<void> {
return this.apiService.send("POST", "/accounts/request-otp", null, true, false);
}
}

View File

@@ -0,0 +1,88 @@
import { CryptoService } from "../../../abstractions/crypto.service";
import { I18nService } from "../../../abstractions/i18n.service";
import { UserVerificationApiServiceAbstraction } from "../../../abstractions/userVerification/userVerification-api.service.abstraction";
import { UserVerificationService as UserVerificationServiceAbstraction } from "../../../abstractions/userVerification/userVerification.service.abstraction";
import { Verification } from "../../../types/verification";
import { VerificationType } from "../../enums/verification-type";
import { SecretVerificationRequest } from "../../models/request/secret-verification.request";
import { VerifyOTPRequest } from "../../models/request/verify-otp.request";
/**
* Used for general-purpose user verification throughout the app.
* Use it to verify the input collected by UserVerificationComponent.
*/
export class UserVerificationService implements UserVerificationServiceAbstraction {
constructor(
private cryptoService: CryptoService,
private i18nService: I18nService,
private userVerificationApiService: UserVerificationApiServiceAbstraction
) {}
/**
* Create a new request model to be used for server-side verification
* @param verification User-supplied verification data (Master Password or OTP)
* @param requestClass The request model to create
* @param alreadyHashed Whether the master password is already hashed
*/
async buildRequest<T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean
) {
this.validateInput(verification);
const request =
requestClass != null ? new requestClass() : (new SecretVerificationRequest() as T);
if (verification.type === VerificationType.OTP) {
request.otp = verification.secret;
} else {
request.masterPasswordHash = alreadyHashed
? verification.secret
: await this.cryptoService.hashPassword(verification.secret, null);
}
return request;
}
/**
* Used to verify the Master Password client-side, or send the OTP to the server for verification (with no other data)
* Generally used for client-side verification only.
* @param verification User-supplied verification data (Master Password or OTP)
*/
async verifyUser(verification: Verification): Promise<boolean> {
this.validateInput(verification);
if (verification.type === VerificationType.OTP) {
const request = new VerifyOTPRequest(verification.secret);
try {
await this.userVerificationApiService.postAccountVerifyOTP(request);
} catch (e) {
throw new Error(this.i18nService.t("invalidVerificationCode"));
}
} else {
const passwordValid = await this.cryptoService.compareAndUpdateKeyHash(
verification.secret,
null
);
if (!passwordValid) {
throw new Error(this.i18nService.t("invalidMasterPassword"));
}
}
return true;
}
async requestOTP() {
await this.userVerificationApiService.postAccountRequestOTP();
}
private validateInput(verification: Verification) {
if (verification?.secret == null || verification.secret === "") {
if (verification.type === VerificationType.OTP) {
throw new Error(this.i18nService.t("verificationCodeRequired"));
} else {
throw new Error(this.i18nService.t("masterPasswordRequired"));
}
}
}
}