mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-03 23:21:29 +01:00
[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 { take } from "rxjs/operators";
|
||||||
|
|
||||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
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 { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService } from "jslib-common/abstractions/cryptoFunction.service";
|
||||||
@ -96,7 +97,13 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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;
|
const response = await this.formPromise;
|
||||||
if (this.rememberEmail || this.alwaysRememberEmail) {
|
if (this.rememberEmail || this.alwaysRememberEmail) {
|
||||||
await this.stateService.setRememberedEmail(this.email);
|
await this.stateService.setRememberedEmail(this.email);
|
||||||
@ -105,7 +112,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
|||||||
}
|
}
|
||||||
if (this.handleCaptchaRequired(response)) {
|
if (this.handleCaptchaRequired(response)) {
|
||||||
return;
|
return;
|
||||||
} else if (response.twoFactor) {
|
} else if (response.requiresTwoFactor) {
|
||||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
this.onSuccessfulLoginTwoFactorNavigate();
|
this.onSuccessfulLoginTwoFactorNavigate();
|
||||||
} else {
|
} else {
|
||||||
|
@ -16,6 +16,7 @@ import { StateService } from "jslib-common/abstractions/state.service";
|
|||||||
import { Utils } from "jslib-common/misc/utils";
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
import { AuthResult } from "jslib-common/models/domain/authResult";
|
||||||
|
import { SsoLogInCredentials } from "jslib-common/models/domain/logInCredentials";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class SsoComponent {
|
export class SsoComponent {
|
||||||
@ -171,14 +172,15 @@ export class SsoComponent {
|
|||||||
private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
|
private async logIn(code: string, codeVerifier: string, orgIdFromState: string) {
|
||||||
this.loggingIn = true;
|
this.loggingIn = true;
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.authService.logInSso(
|
const credentials = new SsoLogInCredentials(
|
||||||
code,
|
code,
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
this.redirectUri,
|
this.redirectUri,
|
||||||
orgIdFromState
|
orgIdFromState
|
||||||
);
|
);
|
||||||
|
this.formPromise = this.authService.logIn(credentials);
|
||||||
const response = await this.formPromise;
|
const response = await this.formPromise;
|
||||||
if (response.twoFactor) {
|
if (response.requiresTwoFactor) {
|
||||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
this.onSuccessfulLoginTwoFactorNavigate();
|
this.onSuccessfulLoginTwoFactorNavigate();
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,6 +6,7 @@ import { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType"
|
|||||||
import { AuthService } from "jslib-common/abstractions/auth.service";
|
import { AuthService } from "jslib-common/abstractions/auth.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
|
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
export class TwoFactorOptionsComponent implements OnInit {
|
export class TwoFactorOptionsComponent implements OnInit {
|
||||||
@ -15,7 +16,7 @@ export class TwoFactorOptionsComponent implements OnInit {
|
|||||||
providers: any[] = [];
|
providers: any[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected authService: AuthService,
|
protected twoFactorService: TwoFactorService,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
@ -23,7 +24,7 @@ export class TwoFactorOptionsComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.providers = this.authService.getSupportedTwoFactorProviders(this.win);
|
this.providers = this.twoFactorService.getSupportedProviders(this.win);
|
||||||
}
|
}
|
||||||
|
|
||||||
choose(p: any) {
|
choose(p: any) {
|
||||||
|
@ -18,9 +18,10 @@ import { LogService } from "jslib-common/abstractions/log.service";
|
|||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { StateService } from "jslib-common/abstractions/state.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 * as DuoWebSDK from "duo_web_sdk";
|
||||||
|
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||||
import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe";
|
import { WebAuthnIFrame } from "jslib-common/misc/webauthn_iframe";
|
||||||
|
|
||||||
@Directive()
|
@Directive()
|
||||||
@ -59,13 +60,14 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
protected environmentService: EnvironmentService,
|
protected environmentService: EnvironmentService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected logService: LogService
|
protected logService: LogService,
|
||||||
|
protected twoFactorService: TwoFactorService
|
||||||
) {
|
) {
|
||||||
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
this.webAuthnSupported = this.platformUtilsService.supportsWebAuthn(win);
|
||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (!this.authing || this.authService.twoFactorProvidersData == null) {
|
if (!this.authing || this.twoFactorService.getProviders() == null) {
|
||||||
this.router.navigate([this.loginRoute]);
|
this.router.navigate([this.loginRoute]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -103,9 +105,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(
|
this.selectedProviderType = this.twoFactorService.getDefaultProvider(this.webAuthnSupported);
|
||||||
this.webAuthnSupported
|
|
||||||
);
|
|
||||||
await this.init();
|
await this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,7 +122,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.cleanupWebAuthn();
|
this.cleanupWebAuthn();
|
||||||
this.title = (TwoFactorProviders as any)[this.selectedProviderType].name;
|
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) {
|
switch (this.selectedProviderType) {
|
||||||
case TwoFactorProviderType.WebAuthn:
|
case TwoFactorProviderType.WebAuthn:
|
||||||
if (!this.webAuthnNewTab) {
|
if (!this.webAuthnNewTab) {
|
||||||
@ -150,7 +150,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
break;
|
break;
|
||||||
case TwoFactorProviderType.Email:
|
case TwoFactorProviderType.Email:
|
||||||
this.twoFactorEmail = providerData.Email;
|
this.twoFactorEmail = providerData.Email;
|
||||||
if (this.authService.twoFactorProvidersData.size > 1) {
|
if (this.twoFactorService.getProviders().size > 1) {
|
||||||
await this.sendEmail(false);
|
await this.sendEmail(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -192,11 +192,11 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doSubmit() {
|
async doSubmit() {
|
||||||
this.formPromise = this.authService.logInTwoFactor(
|
this.formPromise = this.authService.logInTwoFactor({
|
||||||
this.selectedProviderType,
|
provider: this.selectedProviderType,
|
||||||
this.token,
|
token: this.token,
|
||||||
this.remember
|
remember: this.remember,
|
||||||
);
|
});
|
||||||
const response: AuthResult = await this.formPromise;
|
const response: AuthResult = await this.formPromise;
|
||||||
const disableFavicon = await this.stateService.getDisableFavicon();
|
const disableFavicon = await this.stateService.getDisableFavicon();
|
||||||
await this.stateService.setDisableFavicon(!!disableFavicon);
|
await this.stateService.setDisableFavicon(!!disableFavicon);
|
||||||
@ -250,7 +250,7 @@ export class TwoFactorComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
authWebAuthn() {
|
authWebAuthn() {
|
||||||
const providerData = this.authService.twoFactorProvidersData.get(this.selectedProviderType);
|
const providerData = this.twoFactorService.getProviders().get(this.selectedProviderType);
|
||||||
|
|
||||||
if (!this.webAuthnSupported || this.webAuthn == null) {
|
if (!this.webAuthnSupported || this.webAuthn == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -27,6 +27,7 @@ import { StateMigrationService } from "jslib-common/services/stateMigration.serv
|
|||||||
import { SyncService } from "jslib-common/services/sync.service";
|
import { SyncService } from "jslib-common/services/sync.service";
|
||||||
import { TokenService } from "jslib-common/services/token.service";
|
import { TokenService } from "jslib-common/services/token.service";
|
||||||
import { TotpService } from "jslib-common/services/totp.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 { UserVerificationService } from "jslib-common/services/userVerification.service";
|
||||||
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
|
import { VaultTimeoutService } from "jslib-common/services/vaultTimeout.service";
|
||||||
import { WebCryptoFunctionService } from "jslib-common/services/webCryptoFunction.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 { SyncService as SyncServiceAbstraction } from "jslib-common/abstractions/sync.service";
|
||||||
import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "jslib-common/abstractions/token.service";
|
||||||
import { TotpService as TotpServiceAbstraction } from "jslib-common/abstractions/totp.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 { UserVerificationService as UserVerificationServiceAbstraction } from "jslib-common/abstractions/userVerification.service";
|
||||||
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
|
import { VaultTimeoutService as VaultTimeoutServiceAbstraction } from "jslib-common/abstractions/vaultTimeout.service";
|
||||||
|
|
||||||
@ -114,15 +116,13 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
|
|||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
TokenServiceAbstraction,
|
TokenServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
I18nServiceAbstraction,
|
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
VaultTimeoutServiceAbstraction,
|
|
||||||
LogService,
|
LogService,
|
||||||
CryptoFunctionServiceAbstraction,
|
|
||||||
KeyConnectorServiceAbstraction,
|
KeyConnectorServiceAbstraction,
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
|
TwoFactorServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -455,6 +455,7 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
|
|||||||
TokenServiceAbstraction,
|
TokenServiceAbstraction,
|
||||||
LogService,
|
LogService,
|
||||||
OrganizationServiceAbstraction,
|
OrganizationServiceAbstraction,
|
||||||
|
CryptoFunctionServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -473,6 +474,11 @@ import { StateFactory } from "jslib-common/factories/stateFactory";
|
|||||||
useClass: ProviderService,
|
useClass: ProviderService,
|
||||||
deps: [StateServiceAbstraction],
|
deps: [StateServiceAbstraction],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: TwoFactorServiceAbstraction,
|
||||||
|
useClass: TwoFactorService,
|
||||||
|
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
@ -75,7 +75,6 @@ import { SendRequest } from "../models/request/sendRequest";
|
|||||||
import { SetPasswordRequest } from "../models/request/setPasswordRequest";
|
import { SetPasswordRequest } from "../models/request/setPasswordRequest";
|
||||||
import { StorageRequest } from "../models/request/storageRequest";
|
import { StorageRequest } from "../models/request/storageRequest";
|
||||||
import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
|
import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
|
||||||
import { TokenRequest } from "../models/request/tokenRequest";
|
|
||||||
import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
|
import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
|
||||||
import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
|
import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
|
||||||
import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
|
import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
|
||||||
@ -93,6 +92,10 @@ import { VerifyBankRequest } from "../models/request/verifyBankRequest";
|
|||||||
import { VerifyDeleteRecoverRequest } from "../models/request/verifyDeleteRecoverRequest";
|
import { VerifyDeleteRecoverRequest } from "../models/request/verifyDeleteRecoverRequest";
|
||||||
import { VerifyEmailRequest } from "../models/request/verifyEmailRequest";
|
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 { ApiKeyResponse } from "../models/response/apiKeyResponse";
|
||||||
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
import { AttachmentResponse } from "../models/response/attachmentResponse";
|
||||||
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
import { AttachmentUploadDataResponse } from "../models/response/attachmentUploadDataResponse";
|
||||||
@ -171,7 +174,7 @@ import { SendAccessView } from "../models/view/sendAccessView";
|
|||||||
|
|
||||||
export abstract class ApiService {
|
export abstract class ApiService {
|
||||||
postIdentityToken: (
|
postIdentityToken: (
|
||||||
request: TokenRequest
|
request: PasswordTokenRequest | SsoTokenRequest | ApiTokenRequest
|
||||||
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
|
) => Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse>;
|
||||||
refreshIdentityToken: () => Promise<any>;
|
refreshIdentityToken: () => Promise<any>;
|
||||||
|
|
||||||
|
@ -1,58 +1,21 @@
|
|||||||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
|
||||||
|
|
||||||
import { AuthResult } from "../models/domain/authResult";
|
import { AuthResult } from "../models/domain/authResult";
|
||||||
|
import {
|
||||||
|
ApiLogInCredentials,
|
||||||
|
PasswordLogInCredentials,
|
||||||
|
SsoLogInCredentials,
|
||||||
|
} from "../models/domain/logInCredentials";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
export abstract class AuthService {
|
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest";
|
||||||
email: string;
|
|
||||||
masterPasswordHash: string;
|
|
||||||
code: string;
|
|
||||||
codeVerifier: string;
|
|
||||||
ssoRedirectUrl: string;
|
|
||||||
clientId: string;
|
|
||||||
clientSecret: string;
|
|
||||||
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
|
|
||||||
selectedTwoFactorProviderType: TwoFactorProviderType;
|
|
||||||
|
|
||||||
logIn: (email: string, masterPassword: string, captchaToken?: string) => Promise<AuthResult>;
|
export abstract class AuthService {
|
||||||
logInSso: (
|
masterPasswordHash: string;
|
||||||
code: string,
|
email: string;
|
||||||
codeVerifier: string,
|
logIn: (
|
||||||
redirectUrl: string,
|
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||||
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
|
|
||||||
) => Promise<AuthResult>;
|
) => Promise<AuthResult>;
|
||||||
|
logInTwoFactor: (twoFactor: TokenRequestTwoFactor) => Promise<AuthResult>;
|
||||||
logOut: (callback: Function) => void;
|
logOut: (callback: Function) => void;
|
||||||
getSupportedTwoFactorProviders: (win: Window) => any[];
|
|
||||||
getDefaultTwoFactorProvider: (webAuthnSupported: boolean) => TwoFactorProviderType;
|
|
||||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
||||||
authingWithApiKey: () => boolean;
|
authingWithApiKey: () => boolean;
|
||||||
authingWithSso: () => boolean;
|
authingWithSso: () => boolean;
|
||||||
|
@ -1,11 +1,17 @@
|
|||||||
import { Organization } from "../models/domain/organization";
|
import { Organization } from "../models/domain/organization";
|
||||||
|
|
||||||
|
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||||
|
|
||||||
export abstract class KeyConnectorService {
|
export abstract class KeyConnectorService {
|
||||||
getAndSetKey: (url?: string) => Promise<void>;
|
getAndSetKey: (url?: string) => Promise<void>;
|
||||||
getManagingOrganization: () => Promise<Organization>;
|
getManagingOrganization: () => Promise<Organization>;
|
||||||
getUsesKeyConnector: () => Promise<boolean>;
|
getUsesKeyConnector: () => Promise<boolean>;
|
||||||
migrateUser: () => Promise<void>;
|
migrateUser: () => Promise<void>;
|
||||||
userNeedsMigration: () => Promise<boolean>;
|
userNeedsMigration: () => Promise<boolean>;
|
||||||
|
convertNewSsoUserToKeyConnector: (
|
||||||
|
tokenResponse: IdentityTokenResponse,
|
||||||
|
orgId: string
|
||||||
|
) => Promise<void>;
|
||||||
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
||||||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
||||||
getConvertAccountRequired: () => Promise<boolean>;
|
getConvertAccountRequired: () => Promise<boolean>;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||||
|
|
||||||
export abstract class TokenService {
|
export abstract class TokenService {
|
||||||
setTokens: (
|
setTokens: (
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
@ -13,9 +15,9 @@ export abstract class TokenService {
|
|||||||
setClientSecret: (clientSecret: string) => Promise<any>;
|
setClientSecret: (clientSecret: string) => Promise<any>;
|
||||||
getClientSecret: () => Promise<string>;
|
getClientSecret: () => Promise<string>;
|
||||||
toggleTokens: () => Promise<any>;
|
toggleTokens: () => Promise<any>;
|
||||||
setTwoFactorToken: (token: string, email: string) => Promise<any>;
|
setTwoFactorToken: (tokenResponse: IdentityTokenResponse) => Promise<any>;
|
||||||
getTwoFactorToken: (email: string) => Promise<string>;
|
getTwoFactorToken: () => Promise<string>;
|
||||||
clearTwoFactorToken: (email: string) => Promise<any>;
|
clearTwoFactorToken: () => Promise<any>;
|
||||||
clearToken: (userId?: string) => Promise<any>;
|
clearToken: (userId?: string) => Promise<any>;
|
||||||
decodeToken: (token?: string) => any;
|
decodeToken: (token?: string) => any;
|
||||||
getTokenExpirationDate: () => Promise<Date>;
|
getTokenExpirationDate: () => Promise<Date>;
|
||||||
|
24
common/src/abstractions/twoFactor.service.ts
Normal file
24
common/src/abstractions/twoFactor.service.ts
Normal file
@ -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 }>;
|
||||||
|
}
|
73
common/src/misc/logInStrategies/apiLogin.strategy.ts
Normal file
73
common/src/misc/logInStrategies/apiLogin.strategy.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
177
common/src/misc/logInStrategies/logIn.strategy.ts
Normal file
177
common/src/misc/logInStrategies/logIn.strategy.ts
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
88
common/src/misc/logInStrategies/passwordLogin.strategy.ts
Normal file
88
common/src/misc/logInStrategies/passwordLogin.strategy.ts
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
73
common/src/misc/logInStrategies/ssoLogin.strategy.ts
Normal file
73
common/src/misc/logInStrategies/ssoLogin.strategy.ts
Normal file
@ -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 { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
|
||||||
|
|
||||||
|
import { Utils } from "../../misc/utils";
|
||||||
|
|
||||||
export class AuthResult {
|
export class AuthResult {
|
||||||
twoFactor: boolean = false;
|
|
||||||
captchaSiteKey: string = "";
|
captchaSiteKey: string = "";
|
||||||
resetMasterPassword: boolean = false;
|
resetMasterPassword: boolean = false;
|
||||||
forcePasswordReset: boolean = false;
|
forcePasswordReset: boolean = false;
|
||||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
|
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
|
||||||
|
|
||||||
|
get requiresCaptcha() {
|
||||||
|
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
get requiresTwoFactor() {
|
||||||
|
return this.twoFactorProviders != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
24
common/src/models/domain/logInCredentials.ts
Normal file
24
common/src/models/domain/logInCredentials.ts
Normal file
@ -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) {}
|
||||||
|
}
|
24
common/src/models/request/identityToken/apiTokenRequest.ts
Normal file
24
common/src/models/request/identityToken/apiTokenRequest.ts
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
}
|
26
common/src/models/request/identityToken/ssoTokenRequest.ts
Normal file
26
common/src/models/request/identityToken/ssoTokenRequest.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
48
common/src/models/request/identityToken/tokenRequest.ts
Normal file
48
common/src/models/request/identityToken/tokenRequest.ts
Normal file
@ -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 { FolderRequest } from "../models/request/folderRequest";
|
||||||
import { GroupRequest } from "../models/request/groupRequest";
|
import { GroupRequest } from "../models/request/groupRequest";
|
||||||
import { IapCheckRequest } from "../models/request/iapCheckRequest";
|
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 { ImportCiphersRequest } from "../models/request/importCiphersRequest";
|
||||||
import { ImportDirectoryRequest } from "../models/request/importDirectoryRequest";
|
import { ImportDirectoryRequest } from "../models/request/importDirectoryRequest";
|
||||||
import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest";
|
import { ImportOrganizationCiphersRequest } from "../models/request/importOrganizationCiphersRequest";
|
||||||
@ -76,7 +79,6 @@ import { SendRequest } from "../models/request/sendRequest";
|
|||||||
import { SetPasswordRequest } from "../models/request/setPasswordRequest";
|
import { SetPasswordRequest } from "../models/request/setPasswordRequest";
|
||||||
import { StorageRequest } from "../models/request/storageRequest";
|
import { StorageRequest } from "../models/request/storageRequest";
|
||||||
import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
|
import { TaxInfoUpdateRequest } from "../models/request/taxInfoUpdateRequest";
|
||||||
import { TokenRequest } from "../models/request/tokenRequest";
|
|
||||||
import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
|
import { TwoFactorEmailRequest } from "../models/request/twoFactorEmailRequest";
|
||||||
import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
|
import { TwoFactorProviderRequest } from "../models/request/twoFactorProviderRequest";
|
||||||
import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
|
import { TwoFactorRecoveryRequest } from "../models/request/twoFactorRecoveryRequest";
|
||||||
@ -208,7 +210,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
// Auth APIs
|
// Auth APIs
|
||||||
|
|
||||||
async postIdentityToken(
|
async postIdentityToken(
|
||||||
request: TokenRequest
|
request: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest
|
||||||
): Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse> {
|
): Promise<IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse> {
|
||||||
const headers = new Headers({
|
const headers = new Headers({
|
||||||
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
|
"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);
|
headers.set("User-Agent", this.customUserAgent);
|
||||||
}
|
}
|
||||||
request.alterIdentityTokenHeaders(headers);
|
request.alterIdentityTokenHeaders(headers);
|
||||||
|
|
||||||
|
const identityToken =
|
||||||
|
request instanceof ApiTokenRequest
|
||||||
|
? request.toIdentityToken()
|
||||||
|
: request.toIdentityToken(this.platformUtilsService.identityClientId);
|
||||||
|
|
||||||
const response = await this.fetch(
|
const response = await this.fetch(
|
||||||
new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
|
new Request(this.environmentService.getIdentityUrl() + "/connect/token", {
|
||||||
body: this.qsStringify(
|
body: this.qsStringify(identityToken),
|
||||||
request.toIdentityToken(request.clientId ?? this.platformUtilsService.identityClientId)
|
|
||||||
),
|
|
||||||
credentials: this.getCredentials(),
|
credentials: this.getCredentials(),
|
||||||
cache: "no-store",
|
cache: "no-store",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
@ -244,7 +250,7 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
responseJson.TwoFactorProviders2 &&
|
responseJson.TwoFactorProviders2 &&
|
||||||
Object.keys(responseJson.TwoFactorProviders2).length
|
Object.keys(responseJson.TwoFactorProviders2).length
|
||||||
) {
|
) {
|
||||||
await this.tokenService.clearTwoFactorToken(request.email);
|
await this.tokenService.clearTwoFactorToken();
|
||||||
return new IdentityTwoFactorResponse(responseJson);
|
return new IdentityTwoFactorResponse(responseJson);
|
||||||
} else if (
|
} else if (
|
||||||
response.status === 400 &&
|
response.status === 400 &&
|
||||||
|
@ -1,332 +1,125 @@
|
|||||||
import { HashPurpose } from "../enums/hashPurpose";
|
|
||||||
import { KdfType } from "../enums/kdfType";
|
import { KdfType } from "../enums/kdfType";
|
||||||
import { TwoFactorProviderType } from "../enums/twoFactorProviderType";
|
|
||||||
|
|
||||||
import {
|
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
|
||||||
Account,
|
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
|
||||||
AccountData,
|
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
||||||
AccountKeys,
|
|
||||||
AccountProfile,
|
|
||||||
AccountTokens,
|
|
||||||
} from "../models/domain/account";
|
|
||||||
import { AuthResult } from "../models/domain/authResult";
|
import { AuthResult } from "../models/domain/authResult";
|
||||||
|
import {
|
||||||
|
ApiLogInCredentials,
|
||||||
|
PasswordLogInCredentials,
|
||||||
|
SsoLogInCredentials,
|
||||||
|
} from "../models/domain/logInCredentials";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
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 { PreloginRequest } from "../models/request/preloginRequest";
|
||||||
import { TokenRequest } from "../models/request/tokenRequest";
|
|
||||||
|
|
||||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequest";
|
||||||
import { IdentityTwoFactorResponse } from "../models/response/identityTwoFactorResponse";
|
|
||||||
|
|
||||||
import { ApiService } from "../abstractions/api.service";
|
import { ApiService } from "../abstractions/api.service";
|
||||||
import { AppIdService } from "../abstractions/appId.service";
|
import { AppIdService } from "../abstractions/appId.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||||
import { CryptoService } from "../abstractions/crypto.service";
|
import { CryptoService } from "../abstractions/crypto.service";
|
||||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
|
||||||
import { EnvironmentService } from "../abstractions/environment.service";
|
import { EnvironmentService } from "../abstractions/environment.service";
|
||||||
import { I18nService } from "../abstractions/i18n.service";
|
|
||||||
import { KeyConnectorService } from "../abstractions/keyConnector.service";
|
import { KeyConnectorService } from "../abstractions/keyConnector.service";
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
import { MessagingService } from "../abstractions/messaging.service";
|
import { MessagingService } from "../abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { TokenService } from "../abstractions/token.service";
|
import { TokenService } from "../abstractions/token.service";
|
||||||
import { VaultTimeoutService } from "../abstractions/vaultTimeout.service";
|
import { TwoFactorService } from "../abstractions/twoFactor.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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AuthService implements AuthServiceAbstraction {
|
export class AuthService implements AuthServiceAbstraction {
|
||||||
email: string;
|
get email(): string {
|
||||||
masterPasswordHash: string;
|
return this.logInStrategy instanceof PasswordLogInStrategy ? this.logInStrategy.email : null;
|
||||||
localMasterPasswordHash: string;
|
}
|
||||||
code: string;
|
|
||||||
codeVerifier: string;
|
|
||||||
ssoRedirectUrl: string;
|
|
||||||
clientId: string;
|
|
||||||
clientSecret: string;
|
|
||||||
twoFactorProvidersData: Map<TwoFactorProviderType, { [key: string]: string }>;
|
|
||||||
selectedTwoFactorProviderType: TwoFactorProviderType = null;
|
|
||||||
captchaToken: string;
|
|
||||||
|
|
||||||
private key: SymmetricCryptoKey;
|
get masterPasswordHash(): string {
|
||||||
|
return this.logInStrategy instanceof PasswordLogInStrategy
|
||||||
|
? this.logInStrategy.masterPasswordHash
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
protected tokenService: TokenService,
|
protected tokenService: TokenService,
|
||||||
protected appIdService: AppIdService,
|
protected appIdService: AppIdService,
|
||||||
private i18nService: I18nService,
|
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
private messagingService: MessagingService,
|
protected messagingService: MessagingService,
|
||||||
private vaultTimeoutService: VaultTimeoutService,
|
protected logService: LogService,
|
||||||
private logService: LogService,
|
protected keyConnectorService: KeyConnectorService,
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
|
||||||
private keyConnectorService: KeyConnectorService,
|
|
||||||
protected environmentService: EnvironmentService,
|
protected environmentService: EnvironmentService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
private setCryptoKeys = true
|
protected twoFactorService: TwoFactorService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
init() {
|
async logIn(
|
||||||
TwoFactorProviders[TwoFactorProviderType.Email].name = this.i18nService.t("emailTitle");
|
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials
|
||||||
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
|
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
this.selectedTwoFactorProviderType = null;
|
this.clearState();
|
||||||
return await this.logInHelper(
|
|
||||||
null,
|
let result: AuthResult;
|
||||||
null,
|
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
||||||
null,
|
|
||||||
code,
|
if (credentials instanceof PasswordLogInCredentials) {
|
||||||
codeVerifier,
|
strategy = new PasswordLogInStrategy(
|
||||||
redirectUrl,
|
this.cryptoService,
|
||||||
null,
|
this.apiService,
|
||||||
null,
|
this.tokenService,
|
||||||
null,
|
this.appIdService,
|
||||||
null,
|
this.platformUtilsService,
|
||||||
null,
|
this.messagingService,
|
||||||
null,
|
this.logService,
|
||||||
null,
|
this.stateService,
|
||||||
orgId
|
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> {
|
async logInTwoFactor(twoFactor: TokenRequestTwoFactor): Promise<AuthResult> {
|
||||||
this.selectedTwoFactorProviderType = null;
|
try {
|
||||||
return await this.logInHelper(
|
return await this.logInStrategy.logInTwoFactor(twoFactor);
|
||||||
null,
|
} finally {
|
||||||
null,
|
this.clearState();
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logOut(callback: Function) {
|
logOut(callback: Function) {
|
||||||
@ -334,75 +127,16 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
this.messagingService.send("loggedOut");
|
this.messagingService.send("loggedOut");
|
||||||
}
|
}
|
||||||
|
|
||||||
getSupportedTwoFactorProviders(win: Window): any[] {
|
authingWithApiKey(): boolean {
|
||||||
const providers: any[] = [];
|
return this.logInStrategy instanceof ApiLogInStrategy;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultTwoFactorProvider(webAuthnSupported: boolean): TwoFactorProviderType {
|
authingWithSso(): boolean {
|
||||||
if (this.twoFactorProvidersData == null) {
|
return this.logInStrategy instanceof SsoLogInStrategy;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
authingWithPassword(): boolean {
|
||||||
this.selectedTwoFactorProviderType != null &&
|
return this.logInStrategy instanceof PasswordLogInStrategy;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
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);
|
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
|
||||||
}
|
}
|
||||||
|
|
||||||
authingWithApiKey(): boolean {
|
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
|
||||||
return this.clientId != null && this.clientSecret != null;
|
this.logInStrategy = strategy;
|
||||||
}
|
}
|
||||||
|
|
||||||
authingWithSso(): boolean {
|
private clearState() {
|
||||||
return this.code != null && this.codeVerifier != null && this.ssoRedirectUrl != null;
|
this.logInStrategy = 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ApiService } from "../abstractions/api.service";
|
import { ApiService } from "../abstractions/api.service";
|
||||||
import { CryptoService } from "../abstractions/crypto.service";
|
import { CryptoService } from "../abstractions/crypto.service";
|
||||||
|
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
|
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
import { OrganizationService } from "../abstractions/organization.service";
|
import { OrganizationService } from "../abstractions/organization.service";
|
||||||
@ -12,7 +13,11 @@ import { Utils } from "../misc/utils";
|
|||||||
|
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
||||||
|
|
||||||
|
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
||||||
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
|
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
|
||||||
|
import { KeysRequest } from "../models/request/keysRequest";
|
||||||
|
|
||||||
|
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||||
|
|
||||||
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
||||||
constructor(
|
constructor(
|
||||||
@ -21,7 +26,8 @@ export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private organizationService: OrganizationService
|
private organizationService: OrganizationService,
|
||||||
|
private cryptoFunctionService: CryptoFunctionService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
setUsesKeyConnector(usesKeyConnector: boolean) {
|
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) {
|
async setConvertAccountRequired(status: boolean) {
|
||||||
await this.stateService.setConvertAccountToKeyConnector(status);
|
await this.stateService.setConvertAccountToKeyConnector(status);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ import { TokenService as TokenServiceAbstraction } from "../abstractions/token.s
|
|||||||
|
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
|
|
||||||
|
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
||||||
|
|
||||||
export class TokenService implements TokenServiceAbstraction {
|
export class TokenService implements TokenServiceAbstraction {
|
||||||
constructor(private stateService: StateService) {}
|
constructor(private stateService: StateService) {}
|
||||||
|
|
||||||
@ -79,8 +81,8 @@ export class TokenService implements TokenServiceAbstraction {
|
|||||||
await this.setClientSecret(clientSecret);
|
await this.setClientSecret(clientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setTwoFactorToken(token: string): Promise<any> {
|
async setTwoFactorToken(tokenResponse: IdentityTokenResponse): Promise<any> {
|
||||||
return await this.stateService.setTwoFactorToken(token);
|
return await this.stateService.setTwoFactorToken(tokenResponse.twoFactorToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTwoFactorToken(): Promise<string> {
|
async getTwoFactorToken(): Promise<string> {
|
||||||
|
188
common/src/services/twoFactor.service.ts
Normal file
188
common/src/services/twoFactor.service.ts
Normal file
@ -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 { TwoFactorProviderType } from "jslib-common/enums/twoFactorProviderType";
|
||||||
|
|
||||||
import { AuthResult } from "jslib-common/models/domain/authResult";
|
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 { TwoFactorEmailRequest } from "jslib-common/models/request/twoFactorEmailRequest";
|
||||||
import { ErrorResponse } from "jslib-common/models/response/errorResponse";
|
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 { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
import { PolicyService } from "jslib-common/abstractions/policy.service";
|
||||||
import { StateService } from "jslib-common/abstractions/state.service";
|
import { StateService } from "jslib-common/abstractions/state.service";
|
||||||
|
import { TwoFactorService } from "jslib-common/abstractions/twoFactor.service";
|
||||||
|
|
||||||
import { Response } from "../models/response";
|
import { Response } from "../models/response";
|
||||||
|
|
||||||
@ -28,6 +34,8 @@ import { MessageResponse } from "../models/response/messageResponse";
|
|||||||
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
import { NodeUtils } from "jslib-common/misc/nodeUtils";
|
||||||
import { Utils } from "jslib-common/misc/utils";
|
import { Utils } from "jslib-common/misc/utils";
|
||||||
|
|
||||||
|
import Separator from "inquirer/lib/objects/separator";
|
||||||
|
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
const open = require("open");
|
const open = require("open");
|
||||||
|
|
||||||
@ -53,6 +61,7 @@ export class LoginCommand {
|
|||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
protected policyService: PolicyService,
|
protected policyService: PolicyService,
|
||||||
|
protected twoFactorService: TwoFactorService,
|
||||||
clientId: string
|
clientId: string
|
||||||
) {
|
) {
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
@ -143,163 +152,146 @@ export class LoginCommand {
|
|||||||
return Response.error("Invalid two-step login method.");
|
return Response.error("Invalid two-step login method.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const twoFactor =
|
||||||
|
twoFactorToken == null
|
||||||
|
? null
|
||||||
|
: {
|
||||||
|
provider: twoFactorMethod,
|
||||||
|
token: twoFactorToken,
|
||||||
|
remember: false,
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.validatedParams != null) {
|
if (this.validatedParams != null) {
|
||||||
await this.validatedParams();
|
await this.validatedParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: AuthResult = null;
|
let response: AuthResult = null;
|
||||||
if (twoFactorToken != null && twoFactorMethod != null) {
|
if (clientId != null && clientSecret != null) {
|
||||||
if (clientId != null && clientSecret != null) {
|
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
|
||||||
response = await this.authService.logInApiKeyComplete(
|
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
||||||
clientId,
|
response = await this.authService.logIn(
|
||||||
clientSecret,
|
new SsoLogInCredentials(
|
||||||
twoFactorMethod,
|
|
||||||
twoFactorToken,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
|
||||||
response = await this.authService.logInSsoComplete(
|
|
||||||
ssoCode,
|
ssoCode,
|
||||||
ssoCodeVerifier,
|
ssoCodeVerifier,
|
||||||
this.ssoRedirectUri,
|
this.ssoRedirectUri,
|
||||||
twoFactorMethod,
|
orgIdentifier,
|
||||||
twoFactorToken,
|
twoFactor
|
||||||
false
|
)
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
response = await this.authService.logInComplete(
|
|
||||||
email,
|
|
||||||
password,
|
|
||||||
twoFactorMethod,
|
|
||||||
twoFactorToken,
|
|
||||||
false,
|
|
||||||
this.clientSecret
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (clientId != null && clientSecret != null) {
|
response = await this.authService.logIn(
|
||||||
response = await this.authService.logInApiKey(clientId, clientSecret);
|
new PasswordLogInCredentials(email, password, null, twoFactor)
|
||||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
);
|
||||||
response = await this.authService.logInSso(
|
}
|
||||||
ssoCode,
|
if (response.captchaSiteKey) {
|
||||||
ssoCodeVerifier,
|
const badCaptcha = Response.badRequest(
|
||||||
this.ssoRedirectUri,
|
"Your authentication request appears to be coming from a bot\n" +
|
||||||
orgIdentifier
|
"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 = secondResponse;
|
||||||
response = await this.authService.logIn(email, password);
|
} catch (e) {
|
||||||
}
|
|
||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
twoFactorToken == null &&
|
(e instanceof ErrorResponse || e.constructor.name === "ErrorResponse") &&
|
||||||
response.twoFactorProviders.size > 1 &&
|
(e as ErrorResponse).message.includes("Captcha is invalid")
|
||||||
selectedProvider.type === TwoFactorProviderType.Email
|
|
||||||
) {
|
) {
|
||||||
const emailReq = new TwoFactorEmailRequest();
|
return badCaptcha;
|
||||||
emailReq.email = this.authService.email;
|
} else {
|
||||||
emailReq.masterPasswordHash = this.authService.masterPasswordHash;
|
throw e;
|
||||||
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(
|
|
||||||
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.");
|
return Response.error("Login failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
116
spec/common/misc/logInStrategies/apiLogIn.strategy.spec.ts
Normal file
116
spec/common/misc/logInStrategies/apiLogIn.strategy.spec.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
293
spec/common/misc/logInStrategies/logIn.strategy.spec.ts
Normal file
293
spec/common/misc/logInStrategies/logIn.strategy.spec.ts
Normal file
@ -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
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
113
spec/common/misc/logInStrategies/passwordLogIn.strategy.spec.ts
Normal file
113
spec/common/misc/logInStrategies/passwordLogIn.strategy.spec.ts
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
});
|
130
spec/common/misc/logInStrategies/ssoLogIn.strategy.spec.ts
Normal file
130
spec/common/misc/logInStrategies/ssoLogIn.strategy.spec.ts
Normal file
@ -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
Block a user