[Tech debt] Refactor authService and remove LogInHelper (#588)
* Use different strategy classes for different types of login * General refactor and cleanup of auth logic * Create subclasses for different types of login credentials * Create subclasses for different types of tokenRequests * Create TwoFactorService, move code out of authService * refactor base CLI commands to use new interface
This commit is contained in:
parent
92a65b7b36
commit
aa2bdd00be
|
@ -5,6 +5,7 @@ import { Router } from "@angular/router";
|
|||
import { take } from "rxjs/operators";
|
||||
|
||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||
import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||
|
@ -96,7 +97,13 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
|||
}
|
||||
|
||||
try {
|
||||
this.formPromise = this.authService.logIn(this.email, this.masterPassword, this.captchaToken);
|
||||
const credentials = new PasswordLogInCredentials(
|
||||
this.email,
|
||||
this.masterPassword,
|
||||
this.captchaToken,
|
||||
null
|
||||
);
|
||||
this.formPromise = this.authService.logIn(credentials);
|
||||
const response = await this.formPromise;
|
||||
if (this.rememberEmail || this.alwaysRememberEmail) {
|
||||
await this.stateService.setRememberedEmail(this.email);
|
||||
|
@ -105,7 +112,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
|||
}
|
||||
if (this.handleCaptchaRequired(response)) {
|
||||
return;
|
||||
} else if (response.twoFactor) {
|
||||
} else if (response.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
|
|
|
@ -16,6 +16,7 @@ import { StateService } from "jslib-common/abstractions/state.service";
|
|||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||
import { SsoLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||
|
||||
@Directive()
|
||||
export class SsoComponent {
|
||||
|
@ -171,14 +172,15 @@ export class SsoComponent {
|
|||
private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
|
||||
this.loggingIn = true;
|
||||
try {
|
||||
this.formPromise = this.authService.logInSso(
|
||||
const credentials = new SsoLogInCredentials(
|
||||
code,
|
||||
codeVerifier,
|
||||
this.redirectUri,
|
||||
orgIdFromState
|
||||
);
|
||||
this.formPromise = this.authService.logIn(credentials);
|
||||
const response = await this.formPromise;
|
||||
if (response.twoFactor) {
|
||||
if (response.requiresTwoFactor) {
|
||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||
this.onSuccessfulLoginTwoFactorNavigate();
|
||||
} else {
|
||||
|
|
|
@ -6,6 +6,7 @@ import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"
|
|||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
|
||||
@Directive()
|
||||
export class TwoFactorOptionsComponent implements OnInit {
|
||||
|
@ -15,7 +16,7 @@ export class TwoFactorOptionsComponent implements OnInit {
|
|||
providers: any[] = [];
|
||||
|
||||
constructor(
|
||||
protected authService: AuthService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
protected router: Router,
|
||||
protected i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
|
@ -23,7 +24,7 @@ export class TwoFactorOptionsComponent implements OnInit {
|
|||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.providers = this.authService.getSupportedTwoFactorProviders(this.win);
|
||||
this.providers = this.twoFactorService.getSupportedProviders(this.win);
|
||||
}
|
||||
|
||||
choose(p: any) {
|
||||
|
|
|
@ -18,9 +18,10 @@ import { LogService } from "jslib-common/abstractions/log.service";
|
|||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
|
||||
import { TwoFactorProviders } from "jslib-common/services/auth.service";
|
||||
import { TwoFactorProviders } from "jslib-common/services/twoFactor.service";
|
||||
|
||||
import * as DuoWebSDK from "duo_web_sdk";
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe";
|
||||
|
||||
@Directive()
|
||||
|
@ -59,13 +60,14 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||
protected environmentService: EnvironmentService,
|
||||
protected stateService: StateService,
|
||||
protected route: ActivatedRoute,
|
||||
protected logService: LogService
|
||||
protected logService: LogService,
|
||||
protected twoFactorService: TwoFactorService
|
||||
) {
|
||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
if (!this.authing || this.authService.twoFactorProvidersData == null) {
|
||||
if (!this.authing || this.twoFactorService.getProviders() == null) {
|
||||
this.router.navigate([this.loginRoute]);
|
||||
return;
|
||||
}
|
||||
|
@ -103,9 +105,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||
);
|
||||
}
|
||||
|
||||
this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(
|
||||
this.webAuthnSupported
|
||||
);
|
||||
this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported);
|
||||
await this.init();
|
||||
}
|
||||
|
||||
|
@ -122,7 +122,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.cleanupWebAuthn();
|
||||
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
||||
const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType);
|
||||
const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
|
||||
switch (this.selectedProviderType) {
|
||||
case TwoFactorProviderType.WebAuthn:
|
||||
if (!this.webAuthnNewTab) {
|
||||
|
@ -150,7 +150,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||
break;
|
||||
case TwoFactorProviderType.Email:
|
||||
this.twoFactorEmail = providerData.Email;
|
||||
if (this.authService.twoFactorProvidersData.size > 1) {
|
||||
if (this.twoFactorService.getProviders().size > 1) {
|
||||
await this.sendEmail(false);
|
||||
}
|
||||
break;
|
||||
|
@ -192,11 +192,11 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
async doSubmit() {
|
||||
this.formPromise = this.authService.logInTwoFactor(
|
||||
this.selectedProviderType,
|
||||
this.token,
|
||||
this.remember
|
||||
);
|
||||
this.formPromise = this.authService.logInTwoFactor({
|
||||
provider: this.selectedProviderType,
|
||||
token: this.token,
|
||||
remember: this.remember,
|
||||
});
|
||||
const response: AuthResult = await this.formPromise;
|
||||
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||
|
@ -250,7 +250,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
authWebAuthn() {
|
||||
const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType);
|
||||
const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
|
||||
|
||||
if (!this.webAuthnSupported || this.webAuthn == null) {
|
||||
return;
|
||||
|
|
|
@ -27,6 +27,7 @@ import { StateMigrationService } from "jslib-common/services/stateMigration.serv
|
|||
import { SyncService } from "jslib-common/services/sync.service";
|
||||
import { TokenService } from "jslib-common/services/token.service";
|
||||
import { TotpService } from "jslib-common/services/totp.service";
|
||||
import { TwoFactorService } from "jslib-common/services/twoFactor.service";
|
||||
import { UserVerificationService } from "jslib-common/services/userVerification.service";
|
||||
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
|
||||
import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.service";
|
||||
|
@ -65,6 +66,7 @@ import { StorageService as StorageServiceAbstraction } from "jslib-common/abstra
|
|||
import { SyncService as SyncServiceAbstraction } from "jslib-common/abstractions/sync.service";
|
||||
import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
|
||||
import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.service";
|
||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "jslib-common/abstractions/twoFactor.service";
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "jslib-common/abstractions/userVerification.service";
|
||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
|
||||
|
||||
|
@ -114,15 +116,13 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
|
|||
ApiServiceAbstraction,
|
||||
TokenServiceAbstraction,
|
||||
AppIdServiceAbstraction,
|
||||
I18nServiceAbstraction,
|
||||
PlatformUtilsServiceAbstraction,
|
||||
MessagingServiceAbstraction,
|
||||
VaultTimeoutServiceAbstraction,
|
||||
LogService,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
KeyConnectorServiceAbstraction,
|
||||
EnvironmentServiceAbstraction,
|
||||
StateServiceAbstraction,
|
||||
TwoFactorServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -455,6 +455,7 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
|
|||
TokenServiceAbstraction,
|
||||
LogService,
|
||||
OrganizationServiceAbstraction,
|
||||
CryptoFunctionServiceAbstraction,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -473,6 +474,11 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
|
|||
useClass: ProviderService,
|
||||
deps: [StateServiceAbstraction],
|
||||
},
|
||||
{
|
||||
provide: TwoFactorServiceAbstraction,
|
||||
useClass: TwoFactorService,
|
||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class JslibServicesModule {}
|
||||
|
|
|
@ -75,7 +75,6 @@ import { SendRequest } from "../models/request/sendRequest";
|
|||
import { SetPasswordRequest } from "../models/request/setPasswordRequest";
|
||||
import { StorageRequest } from "../models/request/storageRequest";
|
||||
import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
|
||||
import { TokenRequest } from "../models/request/tokenRequest";
|
||||
import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
|
||||
import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
|
||||
import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
|
||||
|
@ -93,6 +92,10 @@ import { VerifyBankRequest } from "../models/request/verifyBankRequest";
|
|||
import { VerifyDeleteRecoverRequest } from "../models/request/verifyDeleteRecoverRequest";
|
||||
import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
|
||||
|
||||
import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
|
||||
import { PasswordTokenRequest } from "../models/request/identityToken/passwordTokenRequest";
|
||||
import { SsoTokenRequest } from "../models/request/identityToken/ssoTokenRequest";
|
||||
|
||||
import { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
||||
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
||||
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
||||
|
@ -171,7 +174,7 @@ import { SendAccessView } from "../models/view/sendAccessView";
|
|||
|
||||
export abstract class ApiService {
|
||||
postIdentityToken: (
|
||||
request: TokenRequest
|
||||
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest
|
||||
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
|
||||
refreshIdentityToken: () => Promise<any>;
|
||||
|
||||
|
|
|
@ -1,58 +1,21 @@
|
|||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
||||
|
||||
import { AuthResult } from "../models/domain/authResult";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "../models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
|
||||
export abstract class AuthService {
|
||||
email: string;
|
||||
masterPasswordHash: string;
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
ssoRedirectUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
|
||||
selectedTwoFactorProviderType: TwoFactorProviderType;
|
||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest";
|
||||
|
||||
logIn: (email: string, masterPassword: string, captchaToken?: string) => Promise<AuthResult>;
|
||||
logInSso: (
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
redirectUrl: string,
|
||||
orgId: string
|
||||
) => Promise<AuthResult>;
|
||||
logInApiKey: (clientId: string, clientSecret: string) => Promise<AuthResult>;
|
||||
logInTwoFactor: (
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean
|
||||
) => Promise<AuthResult>;
|
||||
logInComplete: (
|
||||
email: string,
|
||||
masterPassword: string,
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean,
|
||||
captchaToken?: string
|
||||
) => Promise<AuthResult>;
|
||||
logInSsoComplete: (
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
redirectUrl: string,
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean
|
||||
) => Promise<AuthResult>;
|
||||
logInApiKeyComplete: (
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean
|
||||
export abstract class AuthService {
|
||||
masterPasswordHash: string;
|
||||
email: string;
|
||||
logIn: (
|
||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||
) => Promise<AuthResult>;
|
||||
logInTwoFactor: (twoFactor: TokenRequestTwoFactor) => Promise<AuthResult>;
|
||||
logOut: (callback: Function) => void;
|
||||
getSupportedTwoFactorProviders: (win: Window) => any[];
|
||||
getDefaultTwoFactorProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
|
||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||
authingWithApiKey: () => boolean;
|
||||
authingWithSso: () => boolean;
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import { Organization } from "../models/domain/organization";
|
||||
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export abstract class KeyConnectorService {
|
||||
getAndSetKey: (url?: string) => Promise<void>;
|
||||
getManagingOrganization: () => Promise<Organization>;
|
||||
getUsesKeyConnector: () => Promise<boolean>;
|
||||
migrateUser: () => Promise<void>;
|
||||
userNeedsMigration: () => Promise<boolean>;
|
||||
convertNewSsoUserToKeyConnector: (
|
||||
tokenResponse: IdentityTokenResponse,
|
||||
orgId: string
|
||||
) => Promise<void>;
|
||||
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
||||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
||||
getConvertAccountRequired: () => Promise<boolean>;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export abstract class TokenService {
|
||||
setTokens: (
|
||||
accessToken: string,
|
||||
|
@ -13,9 +15,9 @@ export abstract class TokenService {
|
|||
setClientSecret: (clientSecret: string) => Promise<any>;
|
||||
getClientSecret: () => Promise<string>;
|
||||
toggleTokens: () => Promise<any>;
|
||||
setTwoFactorToken: (token: string, email: string) => Promise<any>;
|
||||
getTwoFactorToken: (email: string) => Promise<string>;
|
||||
clearTwoFactorToken: (email: string) => Promise<any>;
|
||||
setTwoFactorToken: (tokenResponse: IdentityTokenResponse) => Promise<any>;
|
||||
getTwoFactorToken: () => Promise<string>;
|
||||
clearTwoFactorToken: () => Promise<any>;
|
||||
clearToken: (userId?: string) => Promise<any>;
|
||||
decodeToken: (token?: string) => any;
|
||||
getTokenExpirationDate: () => Promise<Date>;
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
||||
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
|
||||
|
||||
export interface TwoFactorProviderDetails {
|
||||
type: TwoFactorProviderType;
|
||||
name: string;
|
||||
description: string;
|
||||
priority: number;
|
||||
sort: number;
|
||||
premium: boolean;
|
||||
}
|
||||
|
||||
export abstract class TwoFactorService {
|
||||
init: () => void;
|
||||
getSupportedProviders: (win: Window) => TwoFactorProviderDetails[];
|
||||
getDefaultProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
|
||||
setSelectedProvider: (type: TwoFactorProviderType) => void;
|
||||
clearSelectedProvider: () => void;
|
||||
|
||||
setProviders: (response: IdentityTwoFactorResponse) => void;
|
||||
clearProviders: () => void;
|
||||
getProviders: () => Map<TwoFactorProviderType, { [key: string]: string }>;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { LogInStrategy } from "./logIn.strategy";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AppIdService } from "../../abstractions/appId.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { EnvironmentService } from "../../abstractions/environment.service";
|
||||
import { KeyConnectorService } from "../../abstractions/keyConnector.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 { TokenService } from "../../abstractions/token.service";
|
||||
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||
|
||||
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
|
||||
|
||||
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
|
||||
|
||||
import { ApiLogInCredentials } from "../../models/domain/logInCredentials";
|
||||
|
||||
export class ApiLogInStrategy extends LogInStrategy {
|
||||
tokenRequest: ApiTokenRequest;
|
||||
|
||||
constructor(
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private environmentService: EnvironmentService,
|
||||
private keyConnectorService: KeyConnectorService
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService
|
||||
);
|
||||
}
|
||||
|
||||
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
|
||||
if (tokenResponse.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(credentials: ApiLogInCredentials) {
|
||||
this.tokenRequest = new ApiTokenRequest(
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
await this.buildTwoFactor(),
|
||||
await this.buildDeviceRequest()
|
||||
);
|
||||
|
||||
return this.startLogIn();
|
||||
}
|
||||
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||
await super.saveAccountInformation(tokenResponse);
|
||||
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
|
||||
await this.stateService.setApiKeyClientSecret(this.tokenRequest.clientSecret);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
|
||||
|
||||
import { Account, AccountProfile, AccountTokens } from "../../models/domain/account";
|
||||
import { AuthResult } from "../../models/domain/authResult";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "../../models/domain/logInCredentials";
|
||||
|
||||
import { DeviceRequest } from "../../models/request/deviceRequest";
|
||||
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
|
||||
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
|
||||
import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest";
|
||||
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequest";
|
||||
import { KeysRequest } from "../../models/request/keysRequest";
|
||||
|
||||
import { IdentityCaptchaResponse } from "../../models/response/identityCaptchaResponse";
|
||||
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
|
||||
import { IdentityTwoFactorResponse } from "../../models/response/identityTwoFactorResponse";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AppIdService } from "../../abstractions/appId.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.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 { TokenService } from "../../abstractions/token.service";
|
||||
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||
|
||||
export abstract class LogInStrategy {
|
||||
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
|
||||
|
||||
constructor(
|
||||
protected cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
protected tokenService: TokenService,
|
||||
protected appIdService: AppIdService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
protected messagingService: MessagingService,
|
||||
protected logService: LogService,
|
||||
protected stateService: StateService,
|
||||
protected twoFactorService: TwoFactorService
|
||||
) {}
|
||||
|
||||
abstract logIn(
|
||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||
): Promise<AuthResult>;
|
||||
|
||||
async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
|
||||
this.tokenRequest.setTwoFactor(twoFactor);
|
||||
return this.startLogIn();
|
||||
}
|
||||
|
||||
protected async startLogIn(): Promise<AuthResult> {
|
||||
this.twoFactorService.clearSelectedProvider();
|
||||
|
||||
const response = await this.apiService.postIdentityToken(this.tokenRequest);
|
||||
|
||||
if (response instanceof IdentityTwoFactorResponse) {
|
||||
return this.processTwoFactorResponse(response);
|
||||
} else if (response instanceof IdentityCaptchaResponse) {
|
||||
return this.processCaptchaResponse(response);
|
||||
} else if (response instanceof IdentityTokenResponse) {
|
||||
return this.processTokenResponse(response);
|
||||
}
|
||||
|
||||
throw new Error("Invalid response object.");
|
||||
}
|
||||
|
||||
protected onSuccessfulLogin(response: IdentityTokenResponse): Promise<void> {
|
||||
// Implemented in subclass if required
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async buildDeviceRequest() {
|
||||
const appId = await this.appIdService.getAppId();
|
||||
return new DeviceRequest(appId, this.platformUtilsService);
|
||||
}
|
||||
|
||||
protected async buildTwoFactor(userProvidedTwoFactor?: TokenRequestTwoFactor) {
|
||||
if (userProvidedTwoFactor != null) {
|
||||
return userProvidedTwoFactor;
|
||||
}
|
||||
|
||||
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken();
|
||||
if (storedTwoFactorToken != null) {
|
||||
return {
|
||||
token: storedTwoFactorToken,
|
||||
provider: TwoFactorProviderType.Remember,
|
||||
remember: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
token: null,
|
||||
provider: null,
|
||||
remember: false,
|
||||
};
|
||||
}
|
||||
|
||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
|
||||
await this.stateService.addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
email: accountInformation.email,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
kdfIterations: tokenResponse.kdfIterations,
|
||||
kdfType: tokenResponse.kdf,
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.resetMasterPassword = response.resetMasterPassword;
|
||||
result.forcePasswordReset = response.forcePasswordReset;
|
||||
|
||||
await this.saveAccountInformation(response);
|
||||
|
||||
if (response.twoFactorToken != null) {
|
||||
await this.tokenService.setTwoFactorToken(response);
|
||||
}
|
||||
|
||||
const newSsoUser = response.key == null;
|
||||
if (!newSsoUser) {
|
||||
await this.cryptoService.setEncKey(response.key);
|
||||
await this.cryptoService.setEncPrivateKey(
|
||||
response.privateKey ?? (await this.createKeyPairForOldAccount())
|
||||
);
|
||||
}
|
||||
|
||||
await this.onSuccessfulLogin(response);
|
||||
|
||||
await this.stateService.setBiometricLocked(false);
|
||||
this.messagingService.send("loggedIn");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async processTwoFactorResponse(response: IdentityTwoFactorResponse): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.twoFactorProviders = response.twoFactorProviders2;
|
||||
this.twoFactorService.setProviders(response);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async processCaptchaResponse(response: IdentityCaptchaResponse): Promise<AuthResult> {
|
||||
const result = new AuthResult();
|
||||
result.captchaSiteKey = response.siteKey;
|
||||
return result;
|
||||
}
|
||||
|
||||
private async createKeyPairForOldAccount() {
|
||||
try {
|
||||
const [publicKey, privateKey] = await this.cryptoService.makeKeyPair();
|
||||
await this.apiService.postAccountKeys(new KeysRequest(publicKey, privateKey.encryptedString));
|
||||
return privateKey.encryptedString;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
import { LogInStrategy } from "./logIn.strategy";
|
||||
|
||||
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AppIdService } from "../../abstractions/appId.service";
|
||||
import { AuthService } from "../../abstractions/auth.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.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 { TokenService } from "../../abstractions/token.service";
|
||||
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||
|
||||
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
||||
|
||||
import { HashPurpose } from "../../enums/hashPurpose";
|
||||
|
||||
export class PasswordLogInStrategy extends LogInStrategy {
|
||||
get email() {
|
||||
return this.tokenRequest.email;
|
||||
}
|
||||
|
||||
get masterPasswordHash() {
|
||||
return this.tokenRequest.masterPasswordHash;
|
||||
}
|
||||
|
||||
tokenRequest: PasswordTokenRequest;
|
||||
|
||||
private localHashedPassword: string;
|
||||
private key: SymmetricCryptoKey;
|
||||
|
||||
constructor(
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private authService: AuthService
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService
|
||||
);
|
||||
}
|
||||
|
||||
async onSuccessfulLogin() {
|
||||
await this.cryptoService.setKey(this.key);
|
||||
await this.cryptoService.setKeyHash(this.localHashedPassword);
|
||||
}
|
||||
|
||||
async logIn(credentials: PasswordLogInCredentials) {
|
||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||
|
||||
this.key = await this.authService.makePreloginKey(masterPassword, email);
|
||||
|
||||
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
|
||||
this.localHashedPassword = await this.cryptoService.hashPassword(
|
||||
masterPassword,
|
||||
this.key,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, this.key);
|
||||
|
||||
this.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
hashedPassword,
|
||||
captchaToken,
|
||||
await this.buildTwoFactor(twoFactor),
|
||||
await this.buildDeviceRequest()
|
||||
);
|
||||
|
||||
return this.startLogIn();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import { LogInStrategy } from "./logIn.strategy";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AppIdService } from "../../abstractions/appId.service";
|
||||
import { CryptoService } from "../../abstractions/crypto.service";
|
||||
import { KeyConnectorService } from "../../abstractions/keyConnector.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 { TokenService } from "../../abstractions/token.service";
|
||||
import { TwoFactorService } from "../../abstractions/twoFactor.service";
|
||||
|
||||
import { SsoLogInCredentials } from "../../models/domain/logInCredentials";
|
||||
|
||||
import { SsoTokenRequest } from "../../models/request/identityToken/ssoTokenRequest";
|
||||
|
||||
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
|
||||
|
||||
export class SsoLogInStrategy extends LogInStrategy {
|
||||
tokenRequest: SsoTokenRequest;
|
||||
orgId: string;
|
||||
|
||||
constructor(
|
||||
cryptoService: CryptoService,
|
||||
apiService: ApiService,
|
||||
tokenService: TokenService,
|
||||
appIdService: AppIdService,
|
||||
platformUtilsService: PlatformUtilsService,
|
||||
messagingService: MessagingService,
|
||||
logService: LogService,
|
||||
stateService: StateService,
|
||||
twoFactorService: TwoFactorService,
|
||||
private keyConnectorService: KeyConnectorService
|
||||
) {
|
||||
super(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService
|
||||
);
|
||||
}
|
||||
|
||||
async onSuccessfulLogin(tokenResponse: IdentityTokenResponse) {
|
||||
const newSsoUser = tokenResponse.key == null;
|
||||
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
if (!newSsoUser) {
|
||||
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
|
||||
} else {
|
||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(credentials: SsoLogInCredentials) {
|
||||
this.orgId = credentials.orgId;
|
||||
this.tokenRequest = new SsoTokenRequest(
|
||||
credentials.code,
|
||||
credentials.codeVerifier,
|
||||
credentials.redirectUrl,
|
||||
await this.buildTwoFactor(credentials.twoFactor),
|
||||
await this.buildDeviceRequest()
|
||||
);
|
||||
|
||||
return this.startLogIn();
|
||||
}
|
||||
}
|
|
@ -1,9 +1,18 @@
|
|||
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
|
||||
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
export class AuthResult {
|
||||
twoFactor: boolean = false;
|
||||
captchaSiteKey: string = "";
|
||||
resetMasterPassword: boolean = false;
|
||||
forcePasswordReset: boolean = false;
|
||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
|
||||
|
||||
get requiresCaptcha() {
|
||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||
}
|
||||
|
||||
get requiresTwoFactor() {
|
||||
return this.twoFactorProviders != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequest";
|
||||
|
||||
export class PasswordLogInCredentials {
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPassword: string,
|
||||
public captchaToken?: string,
|
||||
public twoFactor?: TokenRequestTwoFactor
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SsoLogInCredentials {
|
||||
constructor(
|
||||
public code: string,
|
||||
public codeVerifier: string,
|
||||
public redirectUrl: string,
|
||||
public orgId: string,
|
||||
public twoFactor?: TokenRequestTwoFactor
|
||||
) {}
|
||||
}
|
||||
|
||||
export class ApiLogInCredentials {
|
||||
constructor(public clientId: string, public clientSecret: string) {}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest";
|
||||
|
||||
import { DeviceRequest } from "../deviceRequest";
|
||||
|
||||
export class ApiTokenRequest extends TokenRequest {
|
||||
constructor(
|
||||
public clientId: string,
|
||||
public clientSecret: string,
|
||||
protected twoFactor: TokenRequestTwoFactor,
|
||||
device?: DeviceRequest
|
||||
) {
|
||||
super(twoFactor, device);
|
||||
}
|
||||
|
||||
toIdentityToken() {
|
||||
const obj = super.toIdentityToken(this.clientId);
|
||||
|
||||
obj.scope = this.clientId.startsWith("organization") ? "api.organization" : "api";
|
||||
obj.grant_type = "client_credentials";
|
||||
obj.client_secret = this.clientSecret;
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest";
|
||||
|
||||
import { CaptchaProtectedRequest } from "../captchaProtectedRequest";
|
||||
import { DeviceRequest } from "../deviceRequest";
|
||||
|
||||
import { Utils } from "../../../misc/utils";
|
||||
|
||||
export class PasswordTokenRequest extends TokenRequest implements CaptchaProtectedRequest {
|
||||
constructor(
|
||||
public email: string,
|
||||
public masterPasswordHash: string,
|
||||
public captchaResponse: string,
|
||||
protected twoFactor: TokenRequestTwoFactor,
|
||||
device?: DeviceRequest
|
||||
) {
|
||||
super(twoFactor, device);
|
||||
}
|
||||
|
||||
toIdentityToken(clientId: string) {
|
||||
const obj = super.toIdentityToken(clientId);
|
||||
|
||||
obj.grant_type = "password";
|
||||
obj.username = this.email;
|
||||
obj.password = this.masterPasswordHash;
|
||||
|
||||
if (this.captchaResponse != null) {
|
||||
obj.captchaResponse = this.captchaResponse;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { TokenRequest, TokenRequestTwoFactor } from "./tokenRequest";
|
||||
|
||||
import { DeviceRequest } from "../deviceRequest";
|
||||
|
||||
export class SsoTokenRequest extends TokenRequest {
|
||||
constructor(
|
||||
public code: string,
|
||||
public codeVerifier: string,
|
||||
public redirectUri: string,
|
||||
protected twoFactor: TokenRequestTwoFactor,
|
||||
device?: DeviceRequest
|
||||
) {
|
||||
super(twoFactor, device);
|
||||
}
|
||||
|
||||
toIdentityToken(clientId: string) {
|
||||
const obj = super.toIdentityToken(clientId);
|
||||
|
||||
obj.grant_type = "authorization_code";
|
||||
obj.code = this.code;
|
||||
obj.code_verifier = this.codeVerifier;
|
||||
obj.redirect_uri = this.redirectUri;
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { TwoFactorProviderType } from "../../../enums/twoFactorProviderType";
|
||||
|
||||
import { DeviceRequest } from "../deviceRequest";
|
||||
|
||||
export interface TokenRequestTwoFactor {
|
||||
provider: TwoFactorProviderType;
|
||||
token: string;
|
||||
remember: boolean;
|
||||
}
|
||||
|
||||
export abstract class TokenRequest {
|
||||
protected device?: DeviceRequest;
|
||||
|
||||
constructor(protected twoFactor: TokenRequestTwoFactor, device?: DeviceRequest) {
|
||||
this.device = device != null ? device : null;
|
||||
}
|
||||
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
// Implemented in subclass if required
|
||||
}
|
||||
|
||||
setTwoFactor(twoFactor: TokenRequestTwoFactor) {
|
||||
this.twoFactor = twoFactor;
|
||||
}
|
||||
|
||||
protected toIdentityToken(clientId: string) {
|
||||
const obj: any = {
|
||||
scope: "api offline_access",
|
||||
client_id: clientId,
|
||||
};
|
||||
|
||||
if (this.device) {
|
||||
obj.deviceType = this.device.type;
|
||||
obj.deviceIdentifier = this.device.identifier;
|
||||
obj.deviceName = this.device.name;
|
||||
// no push tokens for browser apps yet
|
||||
// obj.devicePushToken = this.device.pushToken;
|
||||
}
|
||||
|
||||
if (this.twoFactor.token && this.twoFactor.provider != null) {
|
||||
obj.twoFactorToken = this.twoFactor.token;
|
||||
obj.twoFactorProvider = this.twoFactor.provider;
|
||||
obj.twoFactorRemember = this.twoFactor.remember ? "1" : "0";
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
|
||||
|
||||
import { CaptchaProtectedRequest } from "./captchaProtectedRequest";
|
||||
import { DeviceRequest } from "./deviceRequest";
|
||||
|
||||
import { Utils } from "../../misc/utils";
|
||||
|
||||
export class TokenRequest implements CaptchaProtectedRequest {
|
||||
email: string;
|
||||
masterPasswordHash: string;
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
redirectUri: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
device?: DeviceRequest;
|
||||
|
||||
constructor(
|
||||
credentials: string[],
|
||||
codes: string[],
|
||||
clientIdClientSecret: string[],
|
||||
public provider: TwoFactorProviderType,
|
||||
public token: string,
|
||||
public remember: boolean,
|
||||
public captchaResponse: string,
|
||||
device?: DeviceRequest
|
||||
) {
|
||||
if (credentials != null && credentials.length > 1) {
|
||||
this.email = credentials[0];
|
||||
this.masterPasswordHash = credentials[1];
|
||||
} else if (codes != null && codes.length > 2) {
|
||||
this.code = codes[0];
|
||||
this.codeVerifier = codes[1];
|
||||
this.redirectUri = codes[2];
|
||||
} else if (clientIdClientSecret != null && clientIdClientSecret.length > 1) {
|
||||
this.clientId = clientIdClientSecret[0];
|
||||
this.clientSecret = clientIdClientSecret[1];
|
||||
}
|
||||
this.device = device != null ? device : null;
|
||||
}
|
||||
|
||||
toIdentityToken(clientId: string) {
|
||||
const obj: any = {
|
||||
scope: "api offline_access",
|
||||
client_id: clientId,
|
||||
};
|
||||
|
||||
if (this.clientSecret != null) {
|
||||
obj.scope = clientId.startsWith("organization") ? "api.organization" : "api";
|
||||
obj.grant_type = "client_credentials";
|
||||
obj.client_secret = this.clientSecret;
|
||||
} else if (this.masterPasswordHash != null && this.email != null) {
|
||||
obj.grant_type = "password";
|
||||
obj.username = this.email;
|
||||
obj.password = this.masterPasswordHash;
|
||||
} else if (this.code != null && this.codeVerifier != null && this.redirectUri != null) {
|
||||
obj.grant_type = "authorization_code";
|
||||
obj.code = this.code;
|
||||
obj.code_verifier = this.codeVerifier;
|
||||
obj.redirect_uri = this.redirectUri;
|
||||
} else {
|
||||
throw new Error("must provide credentials or codes");
|
||||
}
|
||||
|
||||
if (this.device) {
|
||||
obj.deviceType = this.device.type;
|
||||
obj.deviceIdentifier = this.device.identifier;
|
||||
obj.deviceName = this.device.name;
|
||||
// no push tokens for browser apps yet
|
||||
// obj.devicePushToken = this.device.pushToken;
|
||||
}
|
||||
|
||||
if (this.token && this.provider != null) {
|
||||
obj.twoFactorToken = this.token;
|
||||
obj.twoFactorProvider = this.provider;
|
||||
obj.twoFactorRemember = this.remember ? "1" : "0";
|
||||
}
|
||||
|
||||
if (this.captchaResponse != null) {
|
||||
obj.captchaResponse = this.captchaResponse;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
alterIdentityTokenHeaders(headers: Headers) {
|
||||
if (this.clientSecret == null && this.masterPasswordHash != null && this.email != null) {
|
||||
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,6 +28,9 @@ import { EventRequest } from "../models/request/eventRequest";
|
|||
import { FolderRequest } from "../models/request/folderRequest";
|
||||
import { GroupRequest } from "../models/request/groupRequest";
|
||||
import { IapCheckRequest } from "../models/request/iapCheckRequest";
|
||||
import { ApiTokenRequest } from "../models/request/identityToken/apiTokenRequest";
|
||||
import { PasswordTokenRequest } from "../models/request/identityToken/passwordTokenRequest";
|
||||
import { SsoTokenRequest } from "../models/request/identityToken/ssoTokenRequest";
|
||||
import { ImportCiphersRequest } from "../models/request/importCiphersRequest";
|
||||
import { ImportDirectoryRequest } from "../models/request/importDirectoryRequest";
|
||||
import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest";
|
||||
|
@ -76,7 +79,6 @@ import { SendRequest } from "../models/request/sendRequest";
|
|||
import { SetPasswordRequest } from "../models/request/setPasswordRequest";
|
||||
import { StorageRequest } from "../models/request/storageRequest";
|
||||
import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
|
||||
import { TokenRequest } from "../models/request/tokenRequest";
|
||||
import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
|
||||
import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
|
||||
import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
|
||||
|
@ -208,7 +210,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||
// Auth APIs
|
||||
|
||||
async postIdentityToken(
|
||||
request: TokenRequest
|
||||
request: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest
|
||||
): Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse> {
|
||||
const headers = new Headers({
|
||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
||||
|
@ -219,11 +221,15 @@ export class ApiService implements ApiServiceAbstraction {
|
|||
headers.set("User-Agent", this.customUserAgent);
|
||||
}
|
||||
request.alterIdentityTokenHeaders(headers);
|
||||
|
||||
const identityToken =
|
||||
request instanceof ApiTokenRequest
|
||||
? request.toIdentityToken()
|
||||
: request.toIdentityToken(this.platformUtilsService.identityClientId);
|
||||
|
||||
const response = await this.fetch(
|
||||
new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
|
||||
body: this.qsStringify(
|
||||
request.toIdentityToken(request.clientId ?? this.platformUtilsService.identityClientId)
|
||||
),
|
||||
body: this.qsStringify(identityToken),
|
||||
credentials: this.getCredentials(),
|
||||
cache: "no-store",
|
||||
headers: headers,
|
||||
|
@ -244,7 +250,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||
responseJson.TwoFactorProviders2 &&
|
||||
Object.keys(responseJson.TwoFactorProviders2).length
|
||||
) {
|
||||
await this.tokenService.clearTwoFactorToken(request.email);
|
||||
await this.tokenService.clearTwoFactorToken();
|
||||
return new IdentityTwoFactorResponse(responseJson);
|
||||
} else if (
|
||||
response.status === 400 &&
|
||||
|
|
|
@ -1,332 +1,125 @@
|
|||
import { HashPurpose } from "../enums/hashPurpose";
|
||||
import { KdfType } from "../enums/kdfType";
|
||||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
||||
|
||||
import {
|
||||
Account,
|
||||
AccountData,
|
||||
AccountKeys,
|
||||
AccountProfile,
|
||||
AccountTokens,
|
||||
} from "../models/domain/account";
|
||||
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
|
||||
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
|
||||
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
||||
import { AuthResult } from "../models/domain/authResult";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "../models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
|
||||
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
||||
import { DeviceRequest } from "../models/request/deviceRequest";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
|
||||
import { KeysRequest } from "../models/request/keysRequest";
|
||||
import { PreloginRequest } from "../models/request/preloginRequest";
|
||||
import { TokenRequest } from "../models/request/tokenRequest";
|
||||
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
|
||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest";
|
||||
|
||||
import { ApiService } from "../abstractions/api.service";
|
||||
import { AppIdService } from "../abstractions/appId.service";
|
||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { EnvironmentService } from "../abstractions/environment.service";
|
||||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { KeyConnectorService } from "../abstractions/keyConnector.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 { TokenService } from "../abstractions/token.service";
|
||||
import { VaultTimeoutService } from "../abstractions/vaultTimeout.service";
|
||||
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
export const TwoFactorProviders = {
|
||||
[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,
|
||||
},
|
||||
};
|
||||
import { TwoFactorService } from "../abstractions/twoFactor.service";
|
||||
|
||||
export class AuthService implements AuthServiceAbstraction {
|
||||
email: string;
|
||||
masterPasswordHash: string;
|
||||
localMasterPasswordHash: string;
|
||||
code: string;
|
||||
codeVerifier: string;
|
||||
ssoRedirectUrl: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
|
||||
selectedTwoFactorProviderType: TwoFactorProviderType = null;
|
||||
captchaToken: string;
|
||||
get email(): string {
|
||||
return this.logInStrategy instanceof PasswordLogInStrategy ? this.logInStrategy.email : null;
|
||||
}
|
||||
|
||||
private key: SymmetricCryptoKey;
|
||||
get masterPasswordHash(): string {
|
||||
return this.logInStrategy instanceof PasswordLogInStrategy
|
||||
? this.logInStrategy.masterPasswordHash
|
||||
: null;
|
||||
}
|
||||
|
||||
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
||||
|
||||
constructor(
|
||||
private cryptoService: CryptoService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected apiService: ApiService,
|
||||
protected tokenService: TokenService,
|
||||
protected appIdService: AppIdService,
|
||||
private i18nService: I18nService,
|
||||
protected platformUtilsService: PlatformUtilsService,
|
||||
private messagingService: MessagingService,
|
||||
private vaultTimeoutService: VaultTimeoutService,
|
||||
private logService: LogService,
|
||||
protected cryptoFunctionService: CryptoFunctionService,
|
||||
private keyConnectorService: KeyConnectorService,
|
||||
protected messagingService: MessagingService,
|
||||
protected logService: LogService,
|
||||
protected keyConnectorService: KeyConnectorService,
|
||||
protected environmentService: EnvironmentService,
|
||||
protected stateService: StateService,
|
||||
private setCryptoKeys = true
|
||||
protected twoFactorService: TwoFactorService
|
||||
) {}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
async logIn(email: string, masterPassword: string, captchaToken?: string): Promise<AuthResult> {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
const key = await this.makePreloginKey(masterPassword, email);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
|
||||
const localHashedPassword = await this.cryptoService.hashPassword(
|
||||
masterPassword,
|
||||
key,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
return await this.logInHelper(
|
||||
email,
|
||||
hashedPassword,
|
||||
localHashedPassword,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
key,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
captchaToken,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async logInSso(
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
redirectUrl: string,
|
||||
orgId: string
|
||||
async logIn(
|
||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||
): Promise<AuthResult> {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
return await this.logInHelper(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
code,
|
||||
codeVerifier,
|
||||
redirectUrl,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
orgId
|
||||
);
|
||||
this.clearState();
|
||||
|
||||
let result: AuthResult;
|
||||
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
||||
|
||||
if (credentials instanceof PasswordLogInCredentials) {
|
||||
strategy = new PasswordLogInStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this
|
||||
);
|
||||
result = await strategy.logIn(credentials);
|
||||
} else if (credentials instanceof SsoLogInCredentials) {
|
||||
strategy = new SsoLogInStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.keyConnectorService
|
||||
);
|
||||
result = await strategy.logIn(credentials);
|
||||
} else if (credentials instanceof ApiLogInCredentials) {
|
||||
strategy = new ApiLogInStrategy(
|
||||
this.cryptoService,
|
||||
this.apiService,
|
||||
this.tokenService,
|
||||
this.appIdService,
|
||||
this.platformUtilsService,
|
||||
this.messagingService,
|
||||
this.logService,
|
||||
this.stateService,
|
||||
this.twoFactorService,
|
||||
this.environmentService,
|
||||
this.keyConnectorService
|
||||
);
|
||||
result = await strategy.logIn(credentials);
|
||||
}
|
||||
|
||||
if (result?.requiresTwoFactor) {
|
||||
this.saveState(strategy);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async logInApiKey(clientId: string, clientSecret: string): Promise<AuthResult> {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
return await this.logInHelper(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
clientId,
|
||||
clientSecret,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async logInTwoFactor(
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean
|
||||
): Promise<AuthResult> {
|
||||
return await this.logInHelper(
|
||||
this.email,
|
||||
this.masterPasswordHash,
|
||||
this.localMasterPasswordHash,
|
||||
this.code,
|
||||
this.codeVerifier,
|
||||
this.ssoRedirectUrl,
|
||||
this.clientId,
|
||||
this.clientSecret,
|
||||
this.key,
|
||||
twoFactorProvider,
|
||||
twoFactorToken,
|
||||
remember,
|
||||
this.captchaToken,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async logInComplete(
|
||||
email: string,
|
||||
masterPassword: string,
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean,
|
||||
captchaToken?: string
|
||||
): Promise<AuthResult> {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
const key = await this.makePreloginKey(masterPassword, email);
|
||||
const hashedPassword = await this.cryptoService.hashPassword(masterPassword, key);
|
||||
const localHashedPassword = await this.cryptoService.hashPassword(
|
||||
masterPassword,
|
||||
key,
|
||||
HashPurpose.LocalAuthorization
|
||||
);
|
||||
return await this.logInHelper(
|
||||
email,
|
||||
hashedPassword,
|
||||
localHashedPassword,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
key,
|
||||
twoFactorProvider,
|
||||
twoFactorToken,
|
||||
remember,
|
||||
captchaToken,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async logInSsoComplete(
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
redirectUrl: string,
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean
|
||||
): Promise<AuthResult> {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
return await this.logInHelper(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
code,
|
||||
codeVerifier,
|
||||
redirectUrl,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
twoFactorProvider,
|
||||
twoFactorToken,
|
||||
remember,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
async logInApiKeyComplete(
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
twoFactorProvider: TwoFactorProviderType,
|
||||
twoFactorToken: string,
|
||||
remember?: boolean
|
||||
): Promise<AuthResult> {
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
return await this.logInHelper(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
clientId,
|
||||
clientSecret,
|
||||
null,
|
||||
twoFactorProvider,
|
||||
twoFactorToken,
|
||||
remember,
|
||||
null,
|
||||
null
|
||||
);
|
||||
async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
|
||||
try {
|
||||
return await this.logInStrategy.logInTwoFactor(twoFactor);
|
||||
} finally {
|
||||
this.clearState();
|
||||
}
|
||||
}
|
||||
|
||||
logOut(callback: Function) {
|
||||
|
@ -334,75 +127,16 @@ export class AuthService implements AuthServiceAbstraction {
|
|||
this.messagingService.send("loggedOut");
|
||||
}
|
||||
|
||||
getSupportedTwoFactorProviders(win: Window): any[] {
|
||||
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;
|
||||
authingWithApiKey(): boolean {
|
||||
return this.logInStrategy instanceof ApiLogInStrategy;
|
||||
}
|
||||
|
||||
getDefaultTwoFactorProvider(webAuthnSupported: boolean): TwoFactorProviderType {
|
||||
if (this.twoFactorProvidersData == null) {
|
||||
return null;
|
||||
}
|
||||
authingWithSso(): boolean {
|
||||
return this.logInStrategy instanceof SsoLogInStrategy;
|
||||
}
|
||||
|
||||
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;
|
||||
authingWithPassword(): boolean {
|
||||
return this.logInStrategy instanceof PasswordLogInStrategy;
|
||||
}
|
||||
|
||||
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
||||
|
@ -423,249 +157,11 @@ export class AuthService implements AuthServiceAbstraction {
|
|||
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
|
||||
}
|
||||
|
||||
authingWithApiKey(): boolean {
|
||||
return this.clientId != null && this.clientSecret != null;
|
||||
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
|
||||
this.logInStrategy = strategy;
|
||||
}
|
||||
|
||||
authingWithSso(): boolean {
|
||||
return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null;
|
||||
}
|
||||
|
||||
authingWithPassword(): boolean {
|
||||
return this.email != null && this.masterPasswordHash != null;
|
||||
}
|
||||
|
||||
private async logInHelper(
|
||||
email: string,
|
||||
hashedPassword: string,
|
||||
localHashedPassword: string,
|
||||
code: string,
|
||||
codeVerifier: string,
|
||||
redirectUrl: string,
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
key: SymmetricCryptoKey,
|
||||
twoFactorProvider?: TwoFactorProviderType,
|
||||
twoFactorToken?: string,
|
||||
remember?: boolean,
|
||||
captchaToken?: string,
|
||||
orgId?: string
|
||||
): Promise<AuthResult> {
|
||||
const storedTwoFactorToken = await this.tokenService.getTwoFactorToken(email);
|
||||
const appId = await this.appIdService.getAppId();
|
||||
const deviceRequest = new DeviceRequest(appId, this.platformUtilsService);
|
||||
|
||||
let emailPassword: string[] = [];
|
||||
let codeCodeVerifier: string[] = [];
|
||||
let clientIdClientSecret: [string, string] = [null, null];
|
||||
|
||||
if (email != null && hashedPassword != null) {
|
||||
emailPassword = [email, hashedPassword];
|
||||
} else {
|
||||
emailPassword = null;
|
||||
}
|
||||
if (code != null && codeVerifier != null && redirectUrl != null) {
|
||||
codeCodeVerifier = [code, codeVerifier, redirectUrl];
|
||||
} else {
|
||||
codeCodeVerifier = null;
|
||||
}
|
||||
if (clientId != null && clientSecret != null) {
|
||||
clientIdClientSecret = [clientId, clientSecret];
|
||||
} else {
|
||||
clientIdClientSecret = null;
|
||||
}
|
||||
|
||||
let request: TokenRequest;
|
||||
if (twoFactorToken != null && twoFactorProvider != null) {
|
||||
request = new TokenRequest(
|
||||
emailPassword,
|
||||
codeCodeVerifier,
|
||||
clientIdClientSecret,
|
||||
twoFactorProvider,
|
||||
twoFactorToken,
|
||||
remember,
|
||||
captchaToken,
|
||||
deviceRequest
|
||||
);
|
||||
} else if (storedTwoFactorToken != null) {
|
||||
request = new TokenRequest(
|
||||
emailPassword,
|
||||
codeCodeVerifier,
|
||||
clientIdClientSecret,
|
||||
TwoFactorProviderType.Remember,
|
||||
storedTwoFactorToken,
|
||||
false,
|
||||
captchaToken,
|
||||
deviceRequest
|
||||
);
|
||||
} else {
|
||||
request = new TokenRequest(
|
||||
emailPassword,
|
||||
codeCodeVerifier,
|
||||
clientIdClientSecret,
|
||||
null,
|
||||
null,
|
||||
false,
|
||||
captchaToken,
|
||||
deviceRequest
|
||||
);
|
||||
}
|
||||
|
||||
const response = await this.apiService.postIdentityToken(request);
|
||||
|
||||
this.clearState();
|
||||
const result = new AuthResult();
|
||||
result.captchaSiteKey = (response as any).siteKey;
|
||||
if (!!result.captchaSiteKey) {
|
||||
return result;
|
||||
}
|
||||
result.twoFactor = !!(response as any).twoFactorProviders2;
|
||||
|
||||
if (result.twoFactor) {
|
||||
// two factor required
|
||||
this.email = email;
|
||||
this.masterPasswordHash = hashedPassword;
|
||||
this.localMasterPasswordHash = localHashedPassword;
|
||||
this.code = code;
|
||||
this.codeVerifier = codeVerifier;
|
||||
this.ssoRedirectUrl = redirectUrl;
|
||||
this.clientId = clientId;
|
||||
this.clientSecret = clientSecret;
|
||||
this.key = this.setCryptoKeys ? key : null;
|
||||
const twoFactorResponse = response as IdentityTwoFactorResponse;
|
||||
this.twoFactorProvidersData = twoFactorResponse.twoFactorProviders2;
|
||||
result.twoFactorProviders = twoFactorResponse.twoFactorProviders2;
|
||||
this.captchaToken = twoFactorResponse.captchaToken;
|
||||
return result;
|
||||
}
|
||||
|
||||
const tokenResponse = response as IdentityTokenResponse;
|
||||
result.resetMasterPassword = tokenResponse.resetMasterPassword;
|
||||
result.forcePasswordReset = tokenResponse.forcePasswordReset;
|
||||
|
||||
const accountInformation = await this.tokenService.decodeToken(tokenResponse.accessToken);
|
||||
await this.stateService.addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: accountInformation.sub,
|
||||
email: accountInformation.email,
|
||||
apiKeyClientId: clientId,
|
||||
hasPremiumPersonally: accountInformation.premium,
|
||||
kdfIterations: tokenResponse.kdfIterations,
|
||||
kdfType: tokenResponse.kdf,
|
||||
},
|
||||
},
|
||||
keys: {
|
||||
...new AccountKeys(),
|
||||
...{
|
||||
apiKeyClientSecret: clientSecret,
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: tokenResponse.accessToken,
|
||||
refreshToken: tokenResponse.refreshToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (tokenResponse.twoFactorToken != null) {
|
||||
await this.tokenService.setTwoFactorToken(tokenResponse.twoFactorToken, email);
|
||||
}
|
||||
|
||||
if (this.setCryptoKeys) {
|
||||
if (key != null) {
|
||||
await this.cryptoService.setKey(key);
|
||||
}
|
||||
if (localHashedPassword != null) {
|
||||
await this.cryptoService.setKeyHash(localHashedPassword);
|
||||
}
|
||||
|
||||
// Skip this step during SSO new user flow. No key is returned from server.
|
||||
if (code == null || tokenResponse.key != null) {
|
||||
if (tokenResponse.keyConnectorUrl != null) {
|
||||
await this.keyConnectorService.getAndSetKey(tokenResponse.keyConnectorUrl);
|
||||
} else if (tokenResponse.apiUseKeyConnector) {
|
||||
const keyConnectorUrl = this.environmentService.getKeyConnectorUrl();
|
||||
await this.keyConnectorService.getAndSetKey(keyConnectorUrl);
|
||||
}
|
||||
|
||||
await this.cryptoService.setEncKey(tokenResponse.key);
|
||||
|
||||
// User doesn't have a key pair yet (old account), let's generate one for them
|
||||
if (tokenResponse.privateKey == null) {
|
||||
try {
|
||||
const keyPair = await this.cryptoService.makeKeyPair();
|
||||
await this.apiService.postAccountKeys(
|
||||
new KeysRequest(keyPair[0], keyPair[1].encryptedString)
|
||||
);
|
||||
tokenResponse.privateKey = keyPair[1].encryptedString;
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
await this.cryptoService.setEncPrivateKey(tokenResponse.privateKey);
|
||||
} else if (tokenResponse.keyConnectorUrl != null) {
|
||||
const password = await this.cryptoFunctionService.randomBytes(64);
|
||||
|
||||
const k = await this.cryptoService.makeKey(
|
||||
Utils.fromBufferToB64(password),
|
||||
await this.tokenService.getEmail(),
|
||||
tokenResponse.kdf,
|
||||
tokenResponse.kdfIterations
|
||||
);
|
||||
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(
|
||||
tokenResponse.keyConnectorUrl,
|
||||
keyConnectorRequest
|
||||
);
|
||||
} catch (e) {
|
||||
throw new Error("Unable to reach key connector");
|
||||
}
|
||||
|
||||
const keys = new KeysRequest(pubKey, privKey.encryptedString);
|
||||
const setPasswordRequest = new SetKeyConnectorKeyRequest(
|
||||
encKey[1].encryptedString,
|
||||
tokenResponse.kdf,
|
||||
tokenResponse.kdfIterations,
|
||||
orgId,
|
||||
keys
|
||||
);
|
||||
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.vaultTimeoutService != null) {
|
||||
await this.stateService.setBiometricLocked(false);
|
||||
}
|
||||
this.messagingService.send("loggedIn");
|
||||
return result;
|
||||
}
|
||||
|
||||
private clearState(): void {
|
||||
this.key = null;
|
||||
this.email = null;
|
||||
this.masterPasswordHash = null;
|
||||
this.localMasterPasswordHash = null;
|
||||
this.code = null;
|
||||
this.codeVerifier = null;
|
||||
this.ssoRedirectUrl = null;
|
||||
this.clientId = null;
|
||||
this.clientSecret = null;
|
||||
this.twoFactorProvidersData = null;
|
||||
this.selectedTwoFactorProviderType = null;
|
||||
private clearState() {
|
||||
this.logInStrategy = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { ApiService } from "../abstractions/api.service";
|
||||
import { CryptoService } from "../abstractions/crypto.service";
|
||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
|
||||
import { LogService } from "../abstractions/log.service";
|
||||
import { OrganizationService } from "../abstractions/organization.service";
|
||||
|
@ -12,7 +13,11 @@ import { Utils } from "../misc/utils";
|
|||
|
||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||
|
||||
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
||||
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
|
||||
import { KeysRequest } from "../models/request/keysRequest";
|
||||
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||
constructor(
|
||||
|
@ -21,7 +26,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
private apiService: ApiService,
|
||||
private tokenService: TokenService,
|
||||
private logService: LogService,
|
||||
private organizationService: OrganizationService
|
||||
private organizationService: OrganizationService,
|
||||
private cryptoFunctionService: CryptoFunctionService
|
||||
) {}
|
||||
|
||||
setUsesKeyConnector(usesKeyConnector: boolean) {
|
||||
|
@ -80,6 +86,41 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||
);
|
||||
}
|
||||
|
||||
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
|
||||
const { kdf, kdfIterations, keyConnectorUrl } = tokenResponse;
|
||||
const password = await this.cryptoFunctionService.randomBytes(64);
|
||||
|
||||
const k = await this.cryptoService.makeKey(
|
||||
Utils.fromBufferToB64(password),
|
||||
await this.tokenService.getEmail(),
|
||||
kdf,
|
||||
kdfIterations
|
||||
);
|
||||
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) {
|
||||
throw new Error("Unable to reach key connector");
|
||||
}
|
||||
|
||||
const keys = new KeysRequest(pubKey, privKey.encryptedString);
|
||||
const setPasswordRequest = new SetKeyConnectorKeyRequest(
|
||||
encKey[1].encryptedString,
|
||||
kdf,
|
||||
kdfIterations,
|
||||
orgId,
|
||||
keys
|
||||
);
|
||||
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
|
||||
}
|
||||
|
||||
async setConvertAccountRequired(status: boolean) {
|
||||
await this.stateService.setConvertAccountToKeyConnector(status);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import { TokenService as TokenServiceAbstraction } from "../abstractions/token.s
|
|||
|
||||
import { Utils } from "../misc/utils";
|
||||
|
||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||
|
||||
export class TokenService implements TokenServiceAbstraction {
|
||||
constructor(private stateService: StateService) {}
|
||||
|
||||
|
@ -79,8 +81,8 @@ export class TokenService implements TokenServiceAbstraction {
|
|||
await this.setClientSecret(clientSecret);
|
||||
}
|
||||
|
||||
async setTwoFactorToken(token: string): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(token);
|
||||
async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise<any> {
|
||||
return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken);
|
||||
}
|
||||
|
||||
async getTwoFactorToken(): Promise<string> {
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import { I18nService } from "../abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||
import {
|
||||
TwoFactorProviderDetails,
|
||||
TwoFactorService as TwoFactorServiceAbstraction,
|
||||
} from "../abstractions/twoFactor.service";
|
||||
|
||||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
||||
|
||||
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,11 @@ import * as inquirer from "inquirer";
|
|||
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
||||
|
||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||
import {
|
||||
ApiLogInCredentials,
|
||||
PasswordLogInCredentials,
|
||||
SsoLogInCredentials,
|
||||
} from "jslib-common/models/domain/logInCredentials";
|
||||
import { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
||||
|
||||
|
@ -18,6 +23,7 @@ import { PasswordGenerationService } from "jslib-common/abstractions/passwordGen
|
|||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
|
||||
import { Response } from "../models/response";
|
||||
|
||||
|
@ -28,6 +34,8 @@ import { MessageResponse } from "../models/response/messageResponse";
|
|||
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import Separator from "inquirer/lib/objects/separator";
|
||||
|
||||
// tslint:disable-next-line
|
||||
const open = require("open");
|
||||
|
||||
|
@ -53,6 +61,7 @@ export class LoginCommand {
|
|||
protected stateService: StateService,
|
||||
protected cryptoService: CryptoService,
|
||||
protected policyService: PolicyService,
|
||||
protected twoFactorService: TwoFactorService,
|
||||
clientId: string
|
||||
) {
|
||||
this.clientId = clientId;
|
||||
|
@ -143,163 +152,146 @@ export class LoginCommand {
|
|||
return Response.error("Invalid two-step login method.");
|
||||
}
|
||||
|
||||
const twoFactor =
|
||||
twoFactorToken == null
|
||||
? null
|
||||
: {
|
||||
provider: twoFactorMethod,
|
||||
token: twoFactorToken,
|
||||
remember: false,
|
||||
};
|
||||
|
||||
try {
|
||||
if (this.validatedParams != null) {
|
||||
await this.validatedParams();
|
||||
}
|
||||
|
||||
let response: AuthResult = null;
|
||||
if (twoFactorToken != null && twoFactorMethod != null) {
|
||||
if (clientId != null && clientSecret != null) {
|
||||
response = await this.authService.logInApiKeyComplete(
|
||||
clientId,
|
||||
clientSecret,
|
||||
twoFactorMethod,
|
||||
twoFactorToken,
|
||||
false
|
||||
);
|
||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.authService.logInSsoComplete(
|
||||
if (clientId != null && clientSecret != null) {
|
||||
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
|
||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.authService.logIn(
|
||||
new SsoLogInCredentials(
|
||||
ssoCode,
|
||||
ssoCodeVerifier,
|
||||
this.ssoRedirectUri,
|
||||
twoFactorMethod,
|
||||
twoFactorToken,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
response = await this.authService.logInComplete(
|
||||
email,
|
||||
password,
|
||||
twoFactorMethod,
|
||||
twoFactorToken,
|
||||
false,
|
||||
this.clientSecret
|
||||
);
|
||||
}
|
||||
orgIdentifier,
|
||||
twoFactor
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if (clientId != null && clientSecret != null) {
|
||||
response = await this.authService.logInApiKey(clientId, clientSecret);
|
||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||
response = await this.authService.logInSso(
|
||||
ssoCode,
|
||||
ssoCodeVerifier,
|
||||
this.ssoRedirectUri,
|
||||
orgIdentifier
|
||||
response = await this.authService.logIn(
|
||||
new PasswordLogInCredentials(email, password, null, twoFactor)
|
||||
);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const badCaptcha = Response.badRequest(
|
||||
"Your authentication request appears to be coming from a bot\n" +
|
||||
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
|
||||
"(https://bitwarden.com/help/article/cli-auth-challenges)"
|
||||
);
|
||||
|
||||
try {
|
||||
const captchaClientSecret = await this.apiClientSecret(true);
|
||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
||||
return badCaptcha;
|
||||
}
|
||||
|
||||
const secondResponse = await this.authService.logIn(
|
||||
new PasswordLogInCredentials(email, password, captchaClientSecret, {
|
||||
provider: twoFactorMethod,
|
||||
token: twoFactorToken,
|
||||
remember: false,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
response = await this.authService.logIn(email, password);
|
||||
}
|
||||
if (response.captchaSiteKey) {
|
||||
const badCaptcha = Response.badRequest(
|
||||
"Your authentication request appears to be coming from a bot\n" +
|
||||
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
|
||||
"(https://bitwarden.com/help/article/cli-auth-challenges)"
|
||||
);
|
||||
|
||||
try {
|
||||
const captchaClientSecret = await this.apiClientSecret(true);
|
||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
||||
return badCaptcha;
|
||||
}
|
||||
|
||||
const secondResponse = await this.authService.logInComplete(
|
||||
email,
|
||||
password,
|
||||
twoFactorMethod,
|
||||
twoFactorToken,
|
||||
false,
|
||||
captchaClientSecret
|
||||
);
|
||||
response = secondResponse;
|
||||
} catch (e) {
|
||||
if (
|
||||
(e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") &&
|
||||
(e as ErrorResponse).message.includes("Captcha is invalid")
|
||||
) {
|
||||
return badCaptcha;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (response.twoFactor) {
|
||||
let selectedProvider: any = null;
|
||||
const twoFactorProviders = this.authService.getSupportedTwoFactorProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
return Response.badRequest("No providers available for this client.");
|
||||
}
|
||||
|
||||
if (twoFactorMethod != null) {
|
||||
try {
|
||||
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
|
||||
} catch (e) {
|
||||
return Response.error("Invalid two-step login method.");
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedProvider == null) {
|
||||
if (twoFactorProviders.length === 1) {
|
||||
selectedProvider = twoFactorProviders[0];
|
||||
} else if (this.canInteract) {
|
||||
const twoFactorOptions = twoFactorProviders.map((p) => p.name);
|
||||
twoFactorOptions.push(new inquirer.Separator());
|
||||
twoFactorOptions.push("Cancel");
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "list",
|
||||
name: "method",
|
||||
message: "Two-step login method:",
|
||||
choices: twoFactorOptions,
|
||||
});
|
||||
const i = twoFactorOptions.indexOf(answer.method);
|
||||
if (i === twoFactorOptions.length - 1) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
selectedProvider = twoFactorProviders[i];
|
||||
}
|
||||
if (selectedProvider == null) {
|
||||
return Response.error("Login failed. No provider selected.");
|
||||
}
|
||||
}
|
||||
|
||||
response = secondResponse;
|
||||
} catch (e) {
|
||||
if (
|
||||
twoFactorToken == null &&
|
||||
response.twoFactorProviders.size > 1 &&
|
||||
selectedProvider.type === TwoFactorProviderType.Email
|
||||
(e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") &&
|
||||
(e as ErrorResponse).message.includes("Captcha is invalid")
|
||||
) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = this.authService.email;
|
||||
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
|
||||
await this.apiService.postTwoFactorEmail(emailReq);
|
||||
return badCaptcha;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (twoFactorToken == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "token",
|
||||
message: "Two-step login code:",
|
||||
});
|
||||
twoFactorToken = answer.token;
|
||||
}
|
||||
if (twoFactorToken == null || twoFactorToken === "") {
|
||||
return Response.badRequest("Code is required.");
|
||||
}
|
||||
}
|
||||
|
||||
response = await this.authService.logInTwoFactor(
|
||||
selectedProvider.type,
|
||||
twoFactorToken,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
if (response.requiresTwoFactor) {
|
||||
let selectedProvider: any = null;
|
||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(null);
|
||||
if (twoFactorProviders.length === 0) {
|
||||
return Response.badRequest("No providers available for this client.");
|
||||
}
|
||||
|
||||
if (response.twoFactor) {
|
||||
if (twoFactorMethod != null) {
|
||||
try {
|
||||
selectedProvider = twoFactorProviders.filter((p) => p.type === twoFactorMethod)[0];
|
||||
} catch (e) {
|
||||
return Response.error("Invalid two-step login method.");
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedProvider == null) {
|
||||
if (twoFactorProviders.length === 1) {
|
||||
selectedProvider = twoFactorProviders[0];
|
||||
} else if (this.canInteract) {
|
||||
const twoFactorOptions: (string | Separator)[] = twoFactorProviders.map((p) => p.name);
|
||||
twoFactorOptions.push(new inquirer.Separator());
|
||||
twoFactorOptions.push("Cancel");
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "list",
|
||||
name: "method",
|
||||
message: "Two-step login method:",
|
||||
choices: twoFactorOptions,
|
||||
});
|
||||
const i = twoFactorOptions.indexOf(answer.method);
|
||||
if (i === twoFactorOptions.length - 1) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
selectedProvider = twoFactorProviders[i];
|
||||
}
|
||||
if (selectedProvider == null) {
|
||||
return Response.error("Login failed. No provider selected.");
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
twoFactorToken == null &&
|
||||
response.twoFactorProviders.size > 1 &&
|
||||
selectedProvider.type === TwoFactorProviderType.Email
|
||||
) {
|
||||
const emailReq = new TwoFactorEmailRequest();
|
||||
emailReq.email = this.authService.email;
|
||||
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
|
||||
await this.apiService.postTwoFactorEmail(emailReq);
|
||||
}
|
||||
|
||||
if (twoFactorToken == null) {
|
||||
if (this.canInteract) {
|
||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||
output: process.stderr,
|
||||
})({
|
||||
type: "input",
|
||||
name: "token",
|
||||
message: "Two-step login code:",
|
||||
});
|
||||
twoFactorToken = answer.token;
|
||||
}
|
||||
if (twoFactorToken == null || twoFactorToken === "") {
|
||||
return Response.badRequest("Code is required.");
|
||||
}
|
||||
}
|
||||
|
||||
response = await this.authService.logInTwoFactor({
|
||||
provider: selectedProvider.type,
|
||||
token: twoFactorToken,
|
||||
remember: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (response.requiresTwoFactor) {
|
||||
return Response.error("Login failed.");
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { EnvironmentService } from "jslib-common/abstractions/environment.service";
|
||||
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
|
||||
import { ApiLogInStrategy } from "jslib-common/misc/logInStrategies/apiLogin.strategy";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import { ApiLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
describe("ApiLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let environmentService: SubstituteOf<EnvironmentService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
|
||||
let apiLogInStrategy: ApiLogInStrategy;
|
||||
let credentials: ApiLogInCredentials;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
||||
const apiClientId = "API_CLIENT_ID";
|
||||
const apiClientSecret = "API_CLIENT_SECRET";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
environmentService = Substitute.for<EnvironmentService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
|
||||
apiLogInStrategy = new ApiLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
environmentService,
|
||||
keyConnectorService
|
||||
);
|
||||
|
||||
credentials = new ApiLogInCredentials(apiClientId, apiClientSecret);
|
||||
});
|
||||
|
||||
it("sends api key credentials to the server", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const apiTokenRequest = actual as any;
|
||||
return (
|
||||
apiTokenRequest.clientId === apiClientId &&
|
||||
apiTokenRequest.clientSecret === apiClientSecret &&
|
||||
apiTokenRequest.device.identifier === deviceId &&
|
||||
apiTokenRequest.twoFactor.provider == null &&
|
||||
apiTokenRequest.twoFactor.token == null &&
|
||||
apiTokenRequest.captchaResponse == null
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.received(1).setApiKeyClientId(apiClientId);
|
||||
stateService.received(1).setApiKeyClientSecret(apiClientSecret);
|
||||
stateService.received(1).addAccount(Arg.any());
|
||||
});
|
||||
|
||||
it("gets and sets the Key Connector key from environmentUrl", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.apiUseKeyConnector = true;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
environmentService.getKeyConnectorUrl().returns(keyConnectorUrl);
|
||||
|
||||
await apiLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,293 @@
|
|||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
|
||||
import { PasswordLogInStrategy } from "jslib-common/misc/logInStrategies/passwordLogin.strategy";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import { Account, AccountProfile, AccountTokens } from "jslib-common/models/domain/account";
|
||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||
import { EncString } from "jslib-common/models/domain/encString";
|
||||
import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||
|
||||
import { PasswordTokenRequest } from "jslib-common/models/request/identityToken/passwordTokenRequest";
|
||||
|
||||
import { IdentityCaptchaResponse } from "jslib-common/models/response/identityCaptchaResponse";
|
||||
import { IdentityTokenResponse } from "jslib-common/models/response/identityTokenResponse";
|
||||
import { IdentityTwoFactorResponse } from "jslib-common/models/response/identityTwoFactorResponse";
|
||||
|
||||
import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
||||
|
||||
const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const accessToken = "ACCESS_TOKEN";
|
||||
const refreshToken = "REFRESH_TOKEN";
|
||||
const encKey = "ENC_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const captchaSiteKey = "CAPTCHA_SITE_KEY";
|
||||
const kdf = 0;
|
||||
const kdfIterations = 10000;
|
||||
const userId = Utils.newGuid();
|
||||
const masterPasswordHash = "MASTER_PASSWORD_HASH";
|
||||
|
||||
const decodedToken = {
|
||||
sub: userId,
|
||||
email: email,
|
||||
premium: false,
|
||||
};
|
||||
|
||||
const twoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||
const twoFactorToken = "TWO_FACTOR_TOKEN";
|
||||
const twoFactorRemember = true;
|
||||
|
||||
export function identityTokenResponseFactory() {
|
||||
return new IdentityTokenResponse({
|
||||
ForcePasswordReset: false,
|
||||
Kdf: kdf,
|
||||
KdfIterations: kdfIterations,
|
||||
Key: encKey,
|
||||
PrivateKey: privateKey,
|
||||
ResetMasterPassword: false,
|
||||
access_token: accessToken,
|
||||
expires_in: 3600,
|
||||
refresh_token: refreshToken,
|
||||
scope: "api offline_access",
|
||||
token_type: "Bearer",
|
||||
});
|
||||
}
|
||||
|
||||
describe("LogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
|
||||
// The base class is abstract so we test it via PasswordLogInStrategy
|
||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
authService
|
||||
);
|
||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
||||
});
|
||||
|
||||
describe("base class", () => {
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
tokenService.decodeToken(accessToken).resolves(decodedToken);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.received(1).addAccount(
|
||||
new Account({
|
||||
profile: {
|
||||
...new AccountProfile(),
|
||||
...{
|
||||
userId: userId,
|
||||
email: email,
|
||||
hasPremiumPersonally: false,
|
||||
kdfIterations: kdfIterations,
|
||||
kdfType: kdf,
|
||||
},
|
||||
},
|
||||
tokens: {
|
||||
...new AccountTokens(),
|
||||
...{
|
||||
accessToken: accessToken,
|
||||
refreshToken: refreshToken,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
cryptoService.received(1).setEncKey(encKey);
|
||||
cryptoService.received(1).setEncPrivateKey(privateKey);
|
||||
|
||||
stateService.received(1).setBiometricLocked(false);
|
||||
messagingService.received(1).send("loggedIn");
|
||||
});
|
||||
|
||||
it("builds AuthResult", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.forcePasswordReset = true;
|
||||
tokenResponse.resetMasterPassword = true;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.forcePasswordReset = true;
|
||||
expected.resetMasterPassword = true;
|
||||
expected.twoFactorProviders = null;
|
||||
expected.captchaSiteKey = "";
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("rejects login if CAPTCHA is required", async () => {
|
||||
// Sample CAPTCHA response
|
||||
const tokenResponse = new IdentityCaptchaResponse({
|
||||
error: "invalid_grant",
|
||||
error_description: "Captcha required.",
|
||||
HCaptcha_SiteKey: captchaSiteKey,
|
||||
});
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.didNotReceive().addAccount(Arg.any());
|
||||
messagingService.didNotReceive().send(Arg.any());
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.captchaSiteKey = captchaSiteKey;
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("makes a new public and private key for an old account", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.privateKey = null;
|
||||
cryptoService.makeKeyPair(Arg.any()).resolves(["PUBLIC_KEY", new EncString("PRIVATE_KEY")]);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postAccountKeys(Arg.any());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Two-factor authentication", () => {
|
||||
it("rejects login if 2FA is required", async () => {
|
||||
// Sample response where TOTP 2FA required
|
||||
const tokenResponse = new IdentityTwoFactorResponse({
|
||||
TwoFactorProviders: ["0"],
|
||||
TwoFactorProviders2: { 0: null },
|
||||
error: "invalid_grant",
|
||||
error_description: "Two factor required.",
|
||||
});
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
const result = await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
stateService.didNotReceive().addAccount(Arg.any());
|
||||
messagingService.didNotReceive().send(Arg.any());
|
||||
|
||||
const expected = new AuthResult();
|
||||
expected.twoFactorProviders = new Map<TwoFactorProviderType, { [key: string]: string }>();
|
||||
expected.twoFactorProviders.set(0, null);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it("sends stored 2FA token to server", async () => {
|
||||
tokenService.getTwoFactorToken().resolves(twoFactorToken);
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === TwoFactorProviderType.Remember &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === false
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sends 2FA token provided by user to server (single step)", async () => {
|
||||
// This occurs if the user enters the 2FA code as an argument in the CLI
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
credentials.twoFactor = {
|
||||
provider: twoFactorProviderType,
|
||||
token: twoFactorToken,
|
||||
remember: twoFactorRemember,
|
||||
};
|
||||
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sends 2FA token provided by user to server (two-step)", async () => {
|
||||
// Simulate a partially completed login
|
||||
passwordLogInStrategy.tokenRequest = new PasswordTokenRequest(
|
||||
email,
|
||||
masterPasswordHash,
|
||||
null,
|
||||
null
|
||||
);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await passwordLogInStrategy.logInTwoFactor({
|
||||
provider: twoFactorProviderType,
|
||||
token: twoFactorToken,
|
||||
remember: twoFactorRemember,
|
||||
});
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any;
|
||||
return (
|
||||
passwordTokenRequest.twoFactor.provider === twoFactorProviderType &&
|
||||
passwordTokenRequest.twoFactor.token === twoFactorToken &&
|
||||
passwordTokenRequest.twoFactor.remember === twoFactorRemember
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,113 @@
|
|||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
|
||||
import { PasswordLogInStrategy } from "jslib-common/misc/logInStrategies/passwordLogin.strategy";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import { PasswordLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||
import { SymmetricCryptoKey } from "jslib-common/models/domain/symmetricCryptoKey";
|
||||
|
||||
import { HashPurpose } from "jslib-common/enums/hashPurpose";
|
||||
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
const email = "hello@world.com";
|
||||
const masterPassword = "password";
|
||||
const hashedPassword = "HASHED_PASSWORD";
|
||||
const localHashedPassword = "LOCAL_HASHED_PASSWORD";
|
||||
const preloginKey = new SymmetricCryptoKey(
|
||||
Utils.fromB64ToArray(
|
||||
"N2KWjlLpfi5uHjv+YcfUKIpZ1l+W+6HRensmIqD+BFYBf6N/dvFpJfWwYnVBdgFCK2tJTAIMLhqzIQQEUmGFgg=="
|
||||
)
|
||||
);
|
||||
const deviceId = Utils.newGuid();
|
||||
|
||||
describe("PasswordLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
let authService: SubstituteOf<AuthService>;
|
||||
|
||||
let passwordLogInStrategy: PasswordLogInStrategy;
|
||||
let credentials: PasswordLogInCredentials;
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
authService = Substitute.for<AuthService>();
|
||||
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
|
||||
authService.makePreloginKey(Arg.any(), Arg.any()).resolves(preloginKey);
|
||||
|
||||
cryptoService.hashPassword(masterPassword, Arg.any()).resolves(hashedPassword);
|
||||
cryptoService
|
||||
.hashPassword(masterPassword, Arg.any(), HashPurpose.LocalAuthorization)
|
||||
.resolves(localHashedPassword);
|
||||
|
||||
passwordLogInStrategy = new PasswordLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
authService
|
||||
);
|
||||
credentials = new PasswordLogInCredentials(email, masterPassword);
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
});
|
||||
|
||||
it("sends master password credentials to the server", async () => {
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const passwordTokenRequest = actual as any; // Need to access private fields
|
||||
return (
|
||||
passwordTokenRequest.email === email &&
|
||||
passwordTokenRequest.masterPasswordHash === hashedPassword &&
|
||||
passwordTokenRequest.device.identifier === deviceId &&
|
||||
passwordTokenRequest.twoFactor.provider == null &&
|
||||
passwordTokenRequest.twoFactor.token == null &&
|
||||
passwordTokenRequest.captchaResponse == null
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("sets the local environment after a successful login", async () => {
|
||||
await passwordLogInStrategy.logIn(credentials);
|
||||
|
||||
cryptoService.received(1).setKey(preloginKey);
|
||||
cryptoService.received(1).setKeyHash(localHashedPassword);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { ApiService } from "jslib-common/abstractions/api.service";
|
||||
import { AppIdService } from "jslib-common/abstractions/appId.service";
|
||||
import { CryptoService } from "jslib-common/abstractions/crypto.service";
|
||||
import { KeyConnectorService } from "jslib-common/abstractions/keyConnector.service";
|
||||
import { LogService } from "jslib-common/abstractions/log.service";
|
||||
import { MessagingService } from "jslib-common/abstractions/messaging.service";
|
||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||
import { StateService } from "jslib-common/abstractions/state.service";
|
||||
import { TokenService } from "jslib-common/abstractions/token.service";
|
||||
|
||||
import { SsoLogInStrategy } from "jslib-common/misc/logInStrategies/ssoLogin.strategy";
|
||||
import { Utils } from "jslib-common/misc/utils";
|
||||
|
||||
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||
|
||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
||||
|
||||
import { SsoLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||
|
||||
describe("SsoLogInStrategy", () => {
|
||||
let cryptoService: SubstituteOf<CryptoService>;
|
||||
let apiService: SubstituteOf<ApiService>;
|
||||
let tokenService: SubstituteOf<TokenService>;
|
||||
let appIdService: SubstituteOf<AppIdService>;
|
||||
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||
let messagingService: SubstituteOf<MessagingService>;
|
||||
let logService: SubstituteOf<LogService>;
|
||||
let keyConnectorService: SubstituteOf<KeyConnectorService>;
|
||||
let stateService: SubstituteOf<StateService>;
|
||||
let twoFactorService: SubstituteOf<TwoFactorService>;
|
||||
|
||||
let ssoLogInStrategy: SsoLogInStrategy;
|
||||
let credentials: SsoLogInCredentials;
|
||||
|
||||
const deviceId = Utils.newGuid();
|
||||
const encKey = "ENC_KEY";
|
||||
const privateKey = "PRIVATE_KEY";
|
||||
const keyConnectorUrl = "KEY_CONNECTOR_URL";
|
||||
|
||||
const ssoCode = "SSO_CODE";
|
||||
const ssoCodeVerifier = "SSO_CODE_VERIFIER";
|
||||
const ssoRedirectUrl = "SSO_REDIRECT_URL";
|
||||
const ssoOrgId = "SSO_ORG_ID";
|
||||
|
||||
beforeEach(async () => {
|
||||
cryptoService = Substitute.for<CryptoService>();
|
||||
apiService = Substitute.for<ApiService>();
|
||||
tokenService = Substitute.for<TokenService>();
|
||||
appIdService = Substitute.for<AppIdService>();
|
||||
platformUtilsService = Substitute.for<PlatformUtilsService>();
|
||||
messagingService = Substitute.for<MessagingService>();
|
||||
logService = Substitute.for<LogService>();
|
||||
stateService = Substitute.for<StateService>();
|
||||
keyConnectorService = Substitute.for<KeyConnectorService>();
|
||||
twoFactorService = Substitute.for<TwoFactorService>();
|
||||
|
||||
tokenService.getTwoFactorToken().resolves(null);
|
||||
appIdService.getAppId().resolves(deviceId);
|
||||
|
||||
ssoLogInStrategy = new SsoLogInStrategy(
|
||||
cryptoService,
|
||||
apiService,
|
||||
tokenService,
|
||||
appIdService,
|
||||
platformUtilsService,
|
||||
messagingService,
|
||||
logService,
|
||||
stateService,
|
||||
twoFactorService,
|
||||
keyConnectorService
|
||||
);
|
||||
credentials = new SsoLogInCredentials(ssoCode, ssoCodeVerifier, ssoRedirectUrl, ssoOrgId);
|
||||
});
|
||||
|
||||
it("sends SSO information to server", async () => {
|
||||
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
apiService.received(1).postIdentityToken(
|
||||
Arg.is((actual) => {
|
||||
const ssoTokenRequest = actual as any;
|
||||
return (
|
||||
ssoTokenRequest.code === ssoCode &&
|
||||
ssoTokenRequest.codeVerifier === ssoCodeVerifier &&
|
||||
ssoTokenRequest.redirectUri === ssoRedirectUrl &&
|
||||
ssoTokenRequest.device.identifier === deviceId &&
|
||||
ssoTokenRequest.twoFactor.provider == null &&
|
||||
ssoTokenRequest.twoFactor.token == null
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("does not set keys for new SSO user flow", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.key = null;
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
cryptoService.didNotReceive().setEncPrivateKey(privateKey);
|
||||
cryptoService.didNotReceive().setEncKey(encKey);
|
||||
});
|
||||
|
||||
it("gets and sets KeyConnector key for enrolled user", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).getAndSetKey(keyConnectorUrl);
|
||||
});
|
||||
|
||||
it("converts new SSO user to Key Connector on first login", async () => {
|
||||
const tokenResponse = identityTokenResponseFactory();
|
||||
tokenResponse.keyConnectorUrl = keyConnectorUrl;
|
||||
tokenResponse.key = null;
|
||||
|
||||
apiService.postIdentityToken(Arg.any()).resolves(tokenResponse);
|
||||
|
||||
await ssoLogInStrategy.logIn(credentials);
|
||||
|
||||
keyConnectorService.received(1).convertNewSsoUserToKeyConnector(tokenResponse, ssoOrgId);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue