mirror of
https://github.com/bitwarden/directory-connector
synced 2025-12-05 23:53:21 +00:00
Compare commits
33 Commits
v2024.9.0
...
ac/pm-1180
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01e894b5bc | ||
|
|
aa5a9f3e2f | ||
|
|
f4181b13f7 | ||
|
|
4bb96f049c | ||
|
|
b991fea958 | ||
|
|
111b8bd646 | ||
|
|
42af888615 | ||
|
|
a28fad020b | ||
|
|
f190433348 | ||
|
|
1e211becc3 | ||
|
|
4652c6489f | ||
|
|
9dc497dd13 | ||
|
|
3d9465917d | ||
|
|
6b1b6bf1c4 | ||
|
|
f52af53dad | ||
|
|
6e6039d298 | ||
|
|
332d07eca6 | ||
|
|
c3ed541efd | ||
|
|
4ae4cba877 | ||
|
|
3ecca16f50 | ||
|
|
1f30ef165f | ||
|
|
dfd8fce231 | ||
|
|
0b7c0ec9c2 | ||
|
|
6d569e9319 | ||
|
|
3ed4e76f95 | ||
|
|
abf7e0400c | ||
|
|
5600f20760 | ||
|
|
bbc65d77e3 | ||
|
|
5d0cde9cfa | ||
|
|
eff7c848f8 | ||
|
|
46fb407c0c | ||
|
|
e2fe5ef9ad | ||
|
|
dd10538d0f |
2
.github/workflows/scan.yml
vendored
2
.github/workflows/scan.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
|||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
|
|
||||||
- name: Scan with SonarCloud
|
- name: Scan with SonarCloud
|
||||||
uses: sonarsource/sonarcloud-github-action@e44258b109568baa0df60ed515909fc6c72cba92 # v2.3.0
|
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
15
.github/workflows/version-bump.yml
vendored
15
.github/workflows/version-bump.yml
vendored
@@ -48,8 +48,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
keyvault: "bitwarden-ci"
|
keyvault: "bitwarden-ci"
|
||||||
secrets: "github-gpg-private-key,
|
secrets: "github-gpg-private-key,
|
||||||
github-gpg-private-key-passphrase,
|
github-gpg-private-key-passphrase"
|
||||||
github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
||||||
@@ -150,11 +149,19 @@ jobs:
|
|||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
run: git push -u origin $PR_BRANCH
|
run: git push -u origin $PR_BRANCH
|
||||||
|
|
||||||
|
- name: Generate GH App token
|
||||||
|
uses: actions/create-github-app-token@3378cda945da322a8db4b193e19d46352ebe2de5 # v1.10.4
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.BW_GHAPP_ID }}
|
||||||
|
private-key: ${{ secrets.BW_GHAPP_KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
|
||||||
- name: Create Version PR
|
- name: Create Version PR
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
id: create-pr
|
id: create-pr
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
|
||||||
run: |
|
run: |
|
||||||
@@ -185,7 +192,7 @@ jobs:
|
|||||||
- name: Merge PR
|
- name: Merge PR
|
||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
||||||
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
|
||||||
|
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -26,6 +26,7 @@ npm-debug.log
|
|||||||
# Build directories
|
# Build directories
|
||||||
dist
|
dist
|
||||||
build
|
build
|
||||||
|
build-cli
|
||||||
.angular/cache
|
.angular/cache
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
|
|||||||
@@ -2,41 +2,36 @@ import { LOCALE_ID, NgModule } from "@angular/core";
|
|||||||
|
|
||||||
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
|
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
||||||
import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service";
|
|
||||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/passwordGeneration.service";
|
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { PolicyService as PolicyServiceAbstraction } from "@/jslib/common/src/abstractions/policy.service";
|
|
||||||
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
|
||||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
||||||
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
|
||||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||||
import { Account } from "@/jslib/common/src/models/domain/account";
|
import { Account } from "@/jslib/common/src/models/domain/account";
|
||||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||||
import { ApiService } from "@/jslib/common/src/services/api.service";
|
import { ApiService } from "@/jslib/common/src/services/api.service";
|
||||||
import { AppIdService } from "@/jslib/common/src/services/appId.service";
|
import { AppIdService } from "@/jslib/common/src/services/appId.service";
|
||||||
import { AuthService } from "@/jslib/common/src/services/auth.service";
|
|
||||||
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
|
import { ConsoleLogService } from "@/jslib/common/src/services/consoleLog.service";
|
||||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
|
||||||
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
|
|
||||||
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
|
|
||||||
import { PolicyService } from "@/jslib/common/src/services/policy.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/services/state.service";
|
import { StateService } from "@/jslib/common/src/services/state.service";
|
||||||
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
|
||||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||||
import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service";
|
|
||||||
|
import {
|
||||||
|
SafeInjectionToken,
|
||||||
|
SECURE_STORAGE,
|
||||||
|
WINDOW,
|
||||||
|
} from "../../../../src/app/services/injection-tokens";
|
||||||
|
import { SafeProvider, safeProvider } from "../../../../src/app/services/safe-provider";
|
||||||
|
|
||||||
import { BroadcasterService } from "./broadcaster.service";
|
import { BroadcasterService } from "./broadcaster.service";
|
||||||
import { ModalService } from "./modal.service";
|
import { ModalService } from "./modal.service";
|
||||||
@@ -45,45 +40,31 @@ import { ValidationService } from "./validation.service";
|
|||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: "WINDOW", useValue: window },
|
safeProvider({ provide: WINDOW, useValue: window }),
|
||||||
{
|
safeProvider({
|
||||||
provide: LOCALE_ID,
|
provide: LOCALE_ID as SafeInjectionToken<string>,
|
||||||
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
|
||||||
deps: [I18nServiceAbstraction],
|
deps: [I18nServiceAbstraction],
|
||||||
},
|
}),
|
||||||
ValidationService,
|
safeProvider(ValidationService),
|
||||||
ModalService,
|
safeProvider(ModalService),
|
||||||
{
|
safeProvider({
|
||||||
provide: AppIdServiceAbstraction,
|
provide: AppIdServiceAbstraction,
|
||||||
useClass: AppIdService,
|
useClass: AppIdService,
|
||||||
deps: [StorageServiceAbstraction],
|
deps: [StorageServiceAbstraction],
|
||||||
},
|
}),
|
||||||
{
|
safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
|
||||||
provide: AuthServiceAbstraction,
|
safeProvider({
|
||||||
useClass: AuthService,
|
|
||||||
deps: [
|
|
||||||
CryptoServiceAbstraction,
|
|
||||||
ApiServiceAbstraction,
|
|
||||||
TokenServiceAbstraction,
|
|
||||||
AppIdServiceAbstraction,
|
|
||||||
PlatformUtilsServiceAbstraction,
|
|
||||||
MessagingServiceAbstraction,
|
|
||||||
LogService,
|
|
||||||
KeyConnectorServiceAbstraction,
|
|
||||||
EnvironmentServiceAbstraction,
|
|
||||||
StateServiceAbstraction,
|
|
||||||
TwoFactorServiceAbstraction,
|
|
||||||
I18nServiceAbstraction,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ provide: LogService, useFactory: () => new ConsoleLogService(false) },
|
|
||||||
{
|
|
||||||
provide: EnvironmentServiceAbstraction,
|
provide: EnvironmentServiceAbstraction,
|
||||||
useClass: EnvironmentService,
|
useClass: EnvironmentService,
|
||||||
deps: [StateServiceAbstraction],
|
deps: [StateServiceAbstraction],
|
||||||
},
|
}),
|
||||||
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] },
|
safeProvider({
|
||||||
{
|
provide: TokenServiceAbstraction,
|
||||||
|
useClass: TokenService,
|
||||||
|
deps: [StateServiceAbstraction],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
provide: CryptoServiceAbstraction,
|
provide: CryptoServiceAbstraction,
|
||||||
useClass: CryptoService,
|
useClass: CryptoService,
|
||||||
deps: [
|
deps: [
|
||||||
@@ -92,13 +73,8 @@ import { ValidationService } from "./validation.service";
|
|||||||
LogService,
|
LogService,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
{
|
safeProvider({
|
||||||
provide: PasswordGenerationServiceAbstraction,
|
|
||||||
useClass: PasswordGenerationService,
|
|
||||||
deps: [CryptoServiceAbstraction, PolicyServiceAbstraction, StateServiceAbstraction],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: ApiServiceAbstraction,
|
provide: ApiServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
tokenService: TokenServiceAbstraction,
|
tokenService: TokenServiceAbstraction,
|
||||||
@@ -121,9 +97,13 @@ import { ValidationService } from "./validation.service";
|
|||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService },
|
safeProvider({
|
||||||
{
|
provide: BroadcasterServiceAbstraction,
|
||||||
|
useClass: BroadcasterService,
|
||||||
|
useAngularDecorators: true,
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -140,12 +120,12 @@ import { ValidationService } from "./validation.service";
|
|||||||
),
|
),
|
||||||
deps: [
|
deps: [
|
||||||
StorageServiceAbstraction,
|
StorageServiceAbstraction,
|
||||||
"SECURE_STORAGE",
|
SECURE_STORAGE,
|
||||||
LogService,
|
LogService,
|
||||||
StateMigrationServiceAbstraction,
|
StateMigrationServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
{
|
safeProvider({
|
||||||
provide: StateMigrationServiceAbstraction,
|
provide: StateMigrationServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -156,36 +136,8 @@ import { ValidationService } from "./validation.service";
|
|||||||
secureStorageService,
|
secureStorageService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
),
|
),
|
||||||
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
deps: [StorageServiceAbstraction, SECURE_STORAGE],
|
||||||
},
|
}),
|
||||||
{
|
] satisfies SafeProvider[],
|
||||||
provide: PolicyServiceAbstraction,
|
|
||||||
useClass: PolicyService,
|
|
||||||
deps: [StateServiceAbstraction, OrganizationServiceAbstraction, ApiServiceAbstraction],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: KeyConnectorServiceAbstraction,
|
|
||||||
useClass: KeyConnectorService,
|
|
||||||
deps: [
|
|
||||||
StateServiceAbstraction,
|
|
||||||
CryptoServiceAbstraction,
|
|
||||||
ApiServiceAbstraction,
|
|
||||||
TokenServiceAbstraction,
|
|
||||||
LogService,
|
|
||||||
OrganizationServiceAbstraction,
|
|
||||||
CryptoFunctionServiceAbstraction,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: OrganizationServiceAbstraction,
|
|
||||||
useClass: OrganizationService,
|
|
||||||
deps: [StateServiceAbstraction],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: TwoFactorServiceAbstraction,
|
|
||||||
useClass: TwoFactorService,
|
|
||||||
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class JslibServicesModule {}
|
export class JslibServicesModule {}
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ export class ModalService {
|
|||||||
dialogEl.style.zIndex = `${this.modalCount}050`;
|
dialogEl.style.zIndex = `${this.modalCount}050`;
|
||||||
|
|
||||||
const modals = Array.from(
|
const modals = Array.from(
|
||||||
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]'),
|
el.querySelectorAll('.modal-backdrop, .modal *[data-bs-dismiss="modal"]'),
|
||||||
);
|
);
|
||||||
for (const closeElement of modals) {
|
for (const closeElement of modals) {
|
||||||
closeElement.addEventListener("click", () => {
|
closeElement.addEventListener("click", () => {
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { ApiLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/apiLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { ApiLogInCredentials } from "@/jslib/common/src/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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,288 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
|
|
||||||
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { Account, AccountProfile, AccountTokens } from "@/jslib/common/src/models/domain/account";
|
|
||||||
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
|
|
||||||
import { EncString } from "@/jslib/common/src/models/domain/encString";
|
|
||||||
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
import { PasswordTokenRequest } from "@/jslib/common/src/models/request/identityToken/passwordTokenRequest";
|
|
||||||
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
|
|
||||||
import { IdentityCaptchaResponse } from "@/jslib/common/src/models/response/identityCaptchaResponse";
|
|
||||||
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
|
||||||
import { IdentityTwoFactorResponse } from "@/jslib/common/src/models/response/identityTwoFactorResponse";
|
|
||||||
|
|
||||||
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 = new TokenRequestTwoFactor(
|
|
||||||
twoFactorProviderType,
|
|
||||||
twoFactorToken,
|
|
||||||
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(
|
|
||||||
new TokenRequestTwoFactor(twoFactorProviderType, twoFactorToken, twoFactorRemember),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { HashPurpose } from "@/jslib/common/src/enums/hashPurpose";
|
|
||||||
import { PasswordLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/passwordLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { PasswordLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { SsoLogInStrategy } from "@/jslib/common/src/misc/logInStrategies/ssoLogin.strategy";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { SsoLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
|
|
||||||
import { identityTokenResponseFactory } from "./logIn.strategy.spec";
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { AuthResult } from "../models/domain/authResult";
|
|
||||||
import {
|
|
||||||
ApiLogInCredentials,
|
|
||||||
PasswordLogInCredentials,
|
|
||||||
SsoLogInCredentials,
|
|
||||||
} from "../models/domain/logInCredentials";
|
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
|
||||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
|
||||||
|
|
||||||
export abstract class AuthService {
|
|
||||||
masterPasswordHash: string;
|
|
||||||
email: string;
|
|
||||||
logIn: (
|
|
||||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
|
|
||||||
) => Promise<AuthResult>;
|
|
||||||
logInTwoFactor: (
|
|
||||||
twoFactor: TokenRequestTwoFactor,
|
|
||||||
captchaResponse: string,
|
|
||||||
) => Promise<AuthResult>;
|
|
||||||
logOut: (callback: () => void) => void;
|
|
||||||
makePreloginKey: (masterPassword: string, email: string) => Promise<SymmetricCryptoKey>;
|
|
||||||
authingWithApiKey: () => boolean;
|
|
||||||
authingWithSso: () => boolean;
|
|
||||||
authingWithPassword: () => boolean;
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { Organization } from "../models/domain/organization";
|
|
||||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
|
||||||
|
|
||||||
export abstract class KeyConnectorService {
|
|
||||||
getAndSetKey: (url?: string) => Promise<void>;
|
|
||||||
getManagingOrganization: () => Promise<Organization>;
|
|
||||||
getUsesKeyConnector: () => Promise<boolean>;
|
|
||||||
migrateUser: () => Promise<void>;
|
|
||||||
userNeedsMigration: () => Promise<boolean>;
|
|
||||||
convertNewSsoUserToKeyConnector: (
|
|
||||||
tokenResponse: IdentityTokenResponse,
|
|
||||||
orgId: string,
|
|
||||||
) => Promise<void>;
|
|
||||||
setUsesKeyConnector: (enabled: boolean) => Promise<void>;
|
|
||||||
setConvertAccountRequired: (status: boolean) => Promise<void>;
|
|
||||||
getConvertAccountRequired: () => Promise<boolean>;
|
|
||||||
removeConvertAccountRequired: () => Promise<void>;
|
|
||||||
clear: () => Promise<void>;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { OrganizationData } from "../models/data/organizationData";
|
|
||||||
import { Organization } from "../models/domain/organization";
|
|
||||||
|
|
||||||
export abstract class OrganizationService {
|
|
||||||
get: (id: string) => Promise<Organization>;
|
|
||||||
getByIdentifier: (identifier: string) => Promise<Organization>;
|
|
||||||
getAll: (userId?: string) => Promise<Organization[]>;
|
|
||||||
save: (orgs: { [id: string]: OrganizationData }) => Promise<any>;
|
|
||||||
canManageSponsorships: () => Promise<boolean>;
|
|
||||||
hasOrganizations: (userId?: string) => Promise<boolean>;
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import * as zxcvbn from "zxcvbn";
|
|
||||||
|
|
||||||
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
|
|
||||||
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
|
|
||||||
|
|
||||||
export abstract class PasswordGenerationService {
|
|
||||||
generatePassword: (options: any) => Promise<string>;
|
|
||||||
generatePassphrase: (options: any) => Promise<string>;
|
|
||||||
getOptions: () => Promise<[any, PasswordGeneratorPolicyOptions]>;
|
|
||||||
enforcePasswordGeneratorPoliciesOnOptions: (
|
|
||||||
options: any,
|
|
||||||
) => Promise<[any, PasswordGeneratorPolicyOptions]>;
|
|
||||||
getPasswordGeneratorPolicyOptions: () => Promise<PasswordGeneratorPolicyOptions>;
|
|
||||||
saveOptions: (options: any) => Promise<any>;
|
|
||||||
getHistory: () => Promise<GeneratedPasswordHistory[]>;
|
|
||||||
addHistory: (password: string) => Promise<any>;
|
|
||||||
clear: (userId?: string) => Promise<any>;
|
|
||||||
passwordStrength: (password: string, userInputs?: string[]) => zxcvbn.ZXCVBNResult;
|
|
||||||
normalizeOptions: (options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) => void;
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { PolicyType } from "../enums/policyType";
|
|
||||||
import { PolicyData } from "../models/data/policyData";
|
|
||||||
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
|
|
||||||
import { Policy } from "../models/domain/policy";
|
|
||||||
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
|
|
||||||
import { ListResponse } from "../models/response/listResponse";
|
|
||||||
import { PolicyResponse } from "../models/response/policyResponse";
|
|
||||||
|
|
||||||
export abstract class PolicyService {
|
|
||||||
clearCache: () => void;
|
|
||||||
getAll: (type?: PolicyType, userId?: string) => Promise<Policy[]>;
|
|
||||||
getPolicyForOrganization: (policyType: PolicyType, organizationId: string) => Promise<Policy>;
|
|
||||||
replace: (policies: { [id: string]: PolicyData }) => Promise<any>;
|
|
||||||
clear: (userId?: string) => Promise<any>;
|
|
||||||
getMasterPasswordPoliciesForInvitedUsers: (orgId: string) => Promise<MasterPasswordPolicyOptions>;
|
|
||||||
getMasterPasswordPolicyOptions: (policies?: Policy[]) => Promise<MasterPasswordPolicyOptions>;
|
|
||||||
evaluateMasterPassword: (
|
|
||||||
passwordStrength: number,
|
|
||||||
newPassword: string,
|
|
||||||
enforcedPolicyOptions?: MasterPasswordPolicyOptions,
|
|
||||||
) => boolean;
|
|
||||||
getResetPasswordPolicyOptions: (
|
|
||||||
policies: Policy[],
|
|
||||||
orgId: string,
|
|
||||||
) => [ResetPasswordPolicyOptions, boolean];
|
|
||||||
mapPoliciesFromToken: (policiesResponse: ListResponse<PolicyResponse>) => Policy[];
|
|
||||||
policyAppliesToUser: (
|
|
||||||
policyType: PolicyType,
|
|
||||||
policyFilter?: (policy: Policy) => boolean,
|
|
||||||
userId?: string,
|
|
||||||
) => Promise<boolean>;
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
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 }>;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export enum AuthenticationType {
|
|
||||||
Password = 0,
|
|
||||||
Sso = 1,
|
|
||||||
Api = 2,
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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 { ApiLogInCredentials } from "../../models/domain/logInCredentials";
|
|
||||||
import { ApiTokenRequest } from "../../models/request/identityToken/apiTokenRequest";
|
|
||||||
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
|
|
||||||
|
|
||||||
import { LogInStrategy } from "./logIn.strategy";
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
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";
|
|
||||||
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/tokenRequestTwoFactor";
|
|
||||||
import { KeysRequest } from "../../models/request/keysRequest";
|
|
||||||
import { IdentityCaptchaResponse } from "../../models/response/identityCaptchaResponse";
|
|
||||||
import { IdentityTokenResponse } from "../../models/response/identityTokenResponse";
|
|
||||||
import { IdentityTwoFactorResponse } from "../../models/response/identityTwoFactorResponse";
|
|
||||||
|
|
||||||
export abstract class LogInStrategy {
|
|
||||||
protected abstract tokenRequest: ApiTokenRequest | PasswordTokenRequest | SsoTokenRequest;
|
|
||||||
protected captchaBypassToken: string = null;
|
|
||||||
|
|
||||||
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,
|
|
||||||
captchaResponse: string = null,
|
|
||||||
): 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 new TokenRequestTwoFactor(TwoFactorProviderType.Remember, storedTwoFactorToken, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new TokenRequestTwoFactor();
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
this.captchaBypassToken = response.captchaToken ?? null;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
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 { HashPurpose } from "../../enums/hashPurpose";
|
|
||||||
import { AuthResult } from "../../models/domain/authResult";
|
|
||||||
import { PasswordLogInCredentials } from "../../models/domain/logInCredentials";
|
|
||||||
import { SymmetricCryptoKey } from "../../models/domain/symmetricCryptoKey";
|
|
||||||
import { PasswordTokenRequest } from "../../models/request/identityToken/passwordTokenRequest";
|
|
||||||
import { TokenRequestTwoFactor } from "../../models/request/identityToken/tokenRequestTwoFactor";
|
|
||||||
|
|
||||||
import { LogInStrategy } from "./logIn.strategy";
|
|
||||||
|
|
||||||
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 logInTwoFactor(
|
|
||||||
twoFactor: TokenRequestTwoFactor,
|
|
||||||
captchaResponse: string,
|
|
||||||
): Promise<AuthResult> {
|
|
||||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
|
||||||
return super.logInTwoFactor(twoFactor);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
import { LogInStrategy } from "./logIn.strategy";
|
|
||||||
|
|
||||||
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,17 +0,0 @@
|
|||||||
import { TwoFactorProviderType } from "../../enums/twoFactorProviderType";
|
|
||||||
import { Utils } from "../../misc/utils";
|
|
||||||
|
|
||||||
export class AuthResult {
|
|
||||||
captchaSiteKey = "";
|
|
||||||
resetMasterPassword = false;
|
|
||||||
forcePasswordReset = false;
|
|
||||||
twoFactorProviders: Map<TwoFactorProviderType, { [key: string]: string }> = null;
|
|
||||||
|
|
||||||
get requiresCaptcha() {
|
|
||||||
return !Utils.isNullOrWhitespace(this.captchaSiteKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
get requiresTwoFactor() {
|
|
||||||
return this.twoFactorProviders != null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { AuthenticationType } from "../../enums/authenticationType";
|
|
||||||
import { TokenRequestTwoFactor } from "../request/identityToken/tokenRequestTwoFactor";
|
|
||||||
|
|
||||||
export class PasswordLogInCredentials {
|
|
||||||
readonly type = AuthenticationType.Password;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public email: string,
|
|
||||||
public masterPassword: string,
|
|
||||||
public captchaToken?: string,
|
|
||||||
public twoFactor?: TokenRequestTwoFactor,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class SsoLogInCredentials {
|
|
||||||
readonly type = AuthenticationType.Sso;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public code: string,
|
|
||||||
public codeVerifier: string,
|
|
||||||
public redirectUrl: string,
|
|
||||||
public orgId: string,
|
|
||||||
public twoFactor?: TokenRequestTwoFactor,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ApiLogInCredentials {
|
|
||||||
readonly type = AuthenticationType.Api;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public clientId: string,
|
|
||||||
public clientSecret: string,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,198 +0,0 @@
|
|||||||
import { ApiService } from "../abstractions/api.service";
|
|
||||||
import { AppIdService } from "../abstractions/appId.service";
|
|
||||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
|
||||||
import { CryptoService } from "../abstractions/crypto.service";
|
|
||||||
import { EnvironmentService } from "../abstractions/environment.service";
|
|
||||||
import { I18nService } from "../abstractions/i18n.service";
|
|
||||||
import { KeyConnectorService } from "../abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "../abstractions/log.service";
|
|
||||||
import { MessagingService } from "../abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
|
|
||||||
import { StateService } from "../abstractions/state.service";
|
|
||||||
import { TokenService } from "../abstractions/token.service";
|
|
||||||
import { TwoFactorService } from "../abstractions/twoFactor.service";
|
|
||||||
import { AuthenticationType } from "../enums/authenticationType";
|
|
||||||
import { KdfType } from "../enums/kdfType";
|
|
||||||
import { ApiLogInStrategy } from "../misc/logInStrategies/apiLogin.strategy";
|
|
||||||
import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.strategy";
|
|
||||||
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
|
||||||
import { AuthResult } from "../models/domain/authResult";
|
|
||||||
import {
|
|
||||||
ApiLogInCredentials,
|
|
||||||
PasswordLogInCredentials,
|
|
||||||
SsoLogInCredentials,
|
|
||||||
} from "../models/domain/logInCredentials";
|
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
|
||||||
import { TokenRequestTwoFactor } from "../models/request/identityToken/tokenRequestTwoFactor";
|
|
||||||
import { PreloginRequest } from "../models/request/preloginRequest";
|
|
||||||
import { ErrorResponse } from "../models/response/errorResponse";
|
|
||||||
|
|
||||||
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
|
||||||
|
|
||||||
export class AuthService implements AuthServiceAbstraction {
|
|
||||||
get email(): string {
|
|
||||||
return this.logInStrategy instanceof PasswordLogInStrategy ? this.logInStrategy.email : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get masterPasswordHash(): string {
|
|
||||||
return this.logInStrategy instanceof PasswordLogInStrategy
|
|
||||||
? this.logInStrategy.masterPasswordHash
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private logInStrategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
|
||||||
private sessionTimeout: any;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected cryptoService: CryptoService,
|
|
||||||
protected apiService: ApiService,
|
|
||||||
protected tokenService: TokenService,
|
|
||||||
protected appIdService: AppIdService,
|
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
|
||||||
protected messagingService: MessagingService,
|
|
||||||
protected logService: LogService,
|
|
||||||
protected keyConnectorService: KeyConnectorService,
|
|
||||||
protected environmentService: EnvironmentService,
|
|
||||||
protected stateService: StateService,
|
|
||||||
protected twoFactorService: TwoFactorService,
|
|
||||||
protected i18nService: I18nService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async logIn(
|
|
||||||
credentials: ApiLogInCredentials | PasswordLogInCredentials | SsoLogInCredentials,
|
|
||||||
): Promise<AuthResult> {
|
|
||||||
this.clearState();
|
|
||||||
|
|
||||||
let strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy;
|
|
||||||
|
|
||||||
if (credentials.type === AuthenticationType.Password) {
|
|
||||||
strategy = new PasswordLogInStrategy(
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
} else if (credentials.type === AuthenticationType.Sso) {
|
|
||||||
strategy = new SsoLogInStrategy(
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
this.keyConnectorService,
|
|
||||||
);
|
|
||||||
} else if (credentials.type === AuthenticationType.Api) {
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await strategy.logIn(credentials as any);
|
|
||||||
|
|
||||||
if (result?.requiresTwoFactor) {
|
|
||||||
this.saveState(strategy);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async logInTwoFactor(
|
|
||||||
twoFactor: TokenRequestTwoFactor,
|
|
||||||
captchaResponse: string,
|
|
||||||
): Promise<AuthResult> {
|
|
||||||
if (this.logInStrategy == null) {
|
|
||||||
throw new Error(this.i18nService.t("sessionTimeout"));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
|
|
||||||
|
|
||||||
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
|
|
||||||
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
|
|
||||||
this.clearState();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (e) {
|
|
||||||
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe
|
|
||||||
if (!(e instanceof ErrorResponse)) {
|
|
||||||
this.clearState();
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logOut(callback: () => void) {
|
|
||||||
callback();
|
|
||||||
this.messagingService.send("loggedOut");
|
|
||||||
}
|
|
||||||
|
|
||||||
authingWithApiKey(): boolean {
|
|
||||||
return this.logInStrategy instanceof ApiLogInStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
authingWithSso(): boolean {
|
|
||||||
return this.logInStrategy instanceof SsoLogInStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
authingWithPassword(): boolean {
|
|
||||||
return this.logInStrategy instanceof PasswordLogInStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
async makePreloginKey(masterPassword: string, email: string): Promise<SymmetricCryptoKey> {
|
|
||||||
email = email.trim().toLowerCase();
|
|
||||||
let kdf: KdfType = null;
|
|
||||||
let kdfIterations: number = null;
|
|
||||||
try {
|
|
||||||
const preloginResponse = await this.apiService.postPrelogin(new PreloginRequest(email));
|
|
||||||
if (preloginResponse != null) {
|
|
||||||
kdf = preloginResponse.kdf;
|
|
||||||
kdfIterations = preloginResponse.kdfIterations;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e == null || e.statusCode !== 404) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return this.cryptoService.makeKey(masterPassword, email, kdf, kdfIterations);
|
|
||||||
}
|
|
||||||
|
|
||||||
private saveState(strategy: ApiLogInStrategy | PasswordLogInStrategy | SsoLogInStrategy) {
|
|
||||||
this.logInStrategy = strategy;
|
|
||||||
this.startSessionTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearState() {
|
|
||||||
this.logInStrategy = null;
|
|
||||||
this.clearSessionTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
private startSessionTimeout() {
|
|
||||||
this.clearSessionTimeout();
|
|
||||||
this.sessionTimeout = setTimeout(() => this.clearState(), sessionTimeoutLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
private clearSessionTimeout() {
|
|
||||||
if (this.sessionTimeout != null) {
|
|
||||||
clearTimeout(this.sessionTimeout);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import { ApiService } from "../abstractions/api.service";
|
|
||||||
import { CryptoService } from "../abstractions/crypto.service";
|
|
||||||
import { CryptoFunctionService } from "../abstractions/cryptoFunction.service";
|
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "../abstractions/log.service";
|
|
||||||
import { OrganizationService } from "../abstractions/organization.service";
|
|
||||||
import { StateService } from "../abstractions/state.service";
|
|
||||||
import { TokenService } from "../abstractions/token.service";
|
|
||||||
import { OrganizationUserType } from "../enums/organizationUserType";
|
|
||||||
import { Utils } from "../misc/utils";
|
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetricCryptoKey";
|
|
||||||
import { SetKeyConnectorKeyRequest } from "../models/request/account/setKeyConnectorKeyRequest";
|
|
||||||
import { KeyConnectorUserKeyRequest } from "../models/request/keyConnectorUserKeyRequest";
|
|
||||||
import { KeysRequest } from "../models/request/keysRequest";
|
|
||||||
import { IdentityTokenResponse } from "../models/response/identityTokenResponse";
|
|
||||||
|
|
||||||
export class KeyConnectorService implements KeyConnectorServiceAbstraction {
|
|
||||||
constructor(
|
|
||||||
private stateService: StateService,
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
private tokenService: TokenService,
|
|
||||||
private logService: LogService,
|
|
||||||
private organizationService: OrganizationService,
|
|
||||||
private cryptoFunctionService: CryptoFunctionService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
setUsesKeyConnector(usesKeyConnector: boolean) {
|
|
||||||
return this.stateService.setUsesKeyConnector(usesKeyConnector);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUsesKeyConnector(): Promise<boolean> {
|
|
||||||
return await this.stateService.getUsesKeyConnector();
|
|
||||||
}
|
|
||||||
|
|
||||||
async userNeedsMigration() {
|
|
||||||
const loggedInUsingSso = await this.tokenService.getIsExternal();
|
|
||||||
const requiredByOrganization = (await this.getManagingOrganization()) != null;
|
|
||||||
const userIsNotUsingKeyConnector = !(await this.getUsesKeyConnector());
|
|
||||||
|
|
||||||
return loggedInUsingSso && requiredByOrganization && userIsNotUsingKeyConnector;
|
|
||||||
}
|
|
||||||
|
|
||||||
async migrateUser() {
|
|
||||||
const organization = await this.getManagingOrganization();
|
|
||||||
const key = await this.cryptoService.getKey();
|
|
||||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(key.encKeyB64);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.apiService.postUserKeyToKeyConnector(
|
|
||||||
organization.keyConnectorUrl,
|
|
||||||
keyConnectorRequest,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Unable to reach key connector");
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.apiService.postConvertToKeyConnector();
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAndSetKey(url: string) {
|
|
||||||
try {
|
|
||||||
const userKeyResponse = await this.apiService.getUserKeyFromKeyConnector(url);
|
|
||||||
const keyArr = Utils.fromB64ToArray(userKeyResponse.key);
|
|
||||||
const k = new SymmetricCryptoKey(keyArr);
|
|
||||||
await this.cryptoService.setKey(k);
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
throw new Error("Unable to reach key connector");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getManagingOrganization() {
|
|
||||||
const orgs = await this.organizationService.getAll();
|
|
||||||
return orgs.find(
|
|
||||||
(o) =>
|
|
||||||
o.keyConnectorEnabled &&
|
|
||||||
o.type !== OrganizationUserType.Admin &&
|
|
||||||
o.type !== OrganizationUserType.Owner &&
|
|
||||||
!o.isProviderUser,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async convertNewSsoUserToKeyConnector(tokenResponse: IdentityTokenResponse, orgId: string) {
|
|
||||||
const { kdf, kdfIterations, keyConnectorUrl } = tokenResponse;
|
|
||||||
const password = await this.cryptoFunctionService.randomBytes(64);
|
|
||||||
|
|
||||||
const k = await this.cryptoService.makeKey(
|
|
||||||
Utils.fromBufferToB64(password),
|
|
||||||
await this.tokenService.getEmail(),
|
|
||||||
kdf,
|
|
||||||
kdfIterations,
|
|
||||||
);
|
|
||||||
const keyConnectorRequest = new KeyConnectorUserKeyRequest(k.encKeyB64);
|
|
||||||
await this.cryptoService.setKey(k);
|
|
||||||
|
|
||||||
const encKey = await this.cryptoService.makeEncKey(k);
|
|
||||||
await this.cryptoService.setEncKey(encKey[1].encryptedString);
|
|
||||||
|
|
||||||
const [pubKey, privKey] = await this.cryptoService.makeKeyPair();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.apiService.postUserKeyToKeyConnector(keyConnectorUrl, keyConnectorRequest);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error("Unable to reach key connector");
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = new KeysRequest(pubKey, privKey.encryptedString);
|
|
||||||
const setPasswordRequest = new SetKeyConnectorKeyRequest(
|
|
||||||
encKey[1].encryptedString,
|
|
||||||
kdf,
|
|
||||||
kdfIterations,
|
|
||||||
orgId,
|
|
||||||
keys,
|
|
||||||
);
|
|
||||||
await this.apiService.postSetKeyConnectorKey(setPasswordRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setConvertAccountRequired(status: boolean) {
|
|
||||||
await this.stateService.setConvertAccountToKeyConnector(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getConvertAccountRequired(): Promise<boolean> {
|
|
||||||
return await this.stateService.getConvertAccountToKeyConnector();
|
|
||||||
}
|
|
||||||
|
|
||||||
async removeConvertAccountRequired() {
|
|
||||||
await this.stateService.setConvertAccountToKeyConnector(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clear() {
|
|
||||||
await this.removeConvertAccountRequired();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service";
|
|
||||||
import { StateService } from "../abstractions/state.service";
|
|
||||||
import { OrganizationData } from "../models/data/organizationData";
|
|
||||||
import { Organization } from "../models/domain/organization";
|
|
||||||
|
|
||||||
export class OrganizationService implements OrganizationServiceAbstraction {
|
|
||||||
constructor(private stateService: StateService) {}
|
|
||||||
|
|
||||||
async get(id: string): Promise<Organization> {
|
|
||||||
const organizations = await this.stateService.getOrganizations();
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (organizations == null || !organizations.hasOwnProperty(id)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Organization(organizations[id]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getByIdentifier(identifier: string): Promise<Organization> {
|
|
||||||
const organizations = await this.getAll();
|
|
||||||
if (organizations == null || organizations.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return organizations.find((o) => o.identifier === identifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll(userId?: string): Promise<Organization[]> {
|
|
||||||
const organizations = await this.stateService.getOrganizations({ userId: userId });
|
|
||||||
const response: Organization[] = [];
|
|
||||||
for (const id in organizations) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) {
|
|
||||||
response.push(new Organization(organizations[id]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
async save(organizations: { [id: string]: OrganizationData }) {
|
|
||||||
return await this.stateService.setOrganizations(organizations);
|
|
||||||
}
|
|
||||||
|
|
||||||
async canManageSponsorships(): Promise<boolean> {
|
|
||||||
const orgs = await this.getAll();
|
|
||||||
return orgs.some(
|
|
||||||
(o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async hasOrganizations(userId?: string): Promise<boolean> {
|
|
||||||
const organizations = await this.getAll(userId);
|
|
||||||
return organizations.length > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,572 +0,0 @@
|
|||||||
import * as zxcvbn from "zxcvbn";
|
|
||||||
|
|
||||||
import { CryptoService } from "../abstractions/crypto.service";
|
|
||||||
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "../abstractions/passwordGeneration.service";
|
|
||||||
import { PolicyService } from "../abstractions/policy.service";
|
|
||||||
import { StateService } from "../abstractions/state.service";
|
|
||||||
import { PolicyType } from "../enums/policyType";
|
|
||||||
import { EEFLongWordList } from "../misc/wordlist";
|
|
||||||
import { EncString } from "../models/domain/encString";
|
|
||||||
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
|
|
||||||
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
|
|
||||||
import { Policy } from "../models/domain/policy";
|
|
||||||
|
|
||||||
const DefaultOptions = {
|
|
||||||
length: 14,
|
|
||||||
ambiguous: false,
|
|
||||||
number: true,
|
|
||||||
minNumber: 1,
|
|
||||||
uppercase: true,
|
|
||||||
minUppercase: 0,
|
|
||||||
lowercase: true,
|
|
||||||
minLowercase: 0,
|
|
||||||
special: false,
|
|
||||||
minSpecial: 1,
|
|
||||||
type: "password",
|
|
||||||
numWords: 3,
|
|
||||||
wordSeparator: "-",
|
|
||||||
capitalize: false,
|
|
||||||
includeNumber: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const MaxPasswordsInHistory = 100;
|
|
||||||
|
|
||||||
export class PasswordGenerationService implements PasswordGenerationServiceAbstraction {
|
|
||||||
constructor(
|
|
||||||
private cryptoService: CryptoService,
|
|
||||||
private policyService: PolicyService,
|
|
||||||
private stateService: StateService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async generatePassword(options: any): Promise<string> {
|
|
||||||
// overload defaults with given options
|
|
||||||
const o = Object.assign({}, DefaultOptions, options);
|
|
||||||
|
|
||||||
if (o.type === "passphrase") {
|
|
||||||
return this.generatePassphrase(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitize
|
|
||||||
this.sanitizePasswordLength(o, true);
|
|
||||||
|
|
||||||
const minLength: number = o.minUppercase + o.minLowercase + o.minNumber + o.minSpecial;
|
|
||||||
if (o.length < minLength) {
|
|
||||||
o.length = minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
const positions: string[] = [];
|
|
||||||
if (o.lowercase && o.minLowercase > 0) {
|
|
||||||
for (let i = 0; i < o.minLowercase; i++) {
|
|
||||||
positions.push("l");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (o.uppercase && o.minUppercase > 0) {
|
|
||||||
for (let i = 0; i < o.minUppercase; i++) {
|
|
||||||
positions.push("u");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (o.number && o.minNumber > 0) {
|
|
||||||
for (let i = 0; i < o.minNumber; i++) {
|
|
||||||
positions.push("n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (o.special && o.minSpecial > 0) {
|
|
||||||
for (let i = 0; i < o.minSpecial; i++) {
|
|
||||||
positions.push("s");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
while (positions.length < o.length) {
|
|
||||||
positions.push("a");
|
|
||||||
}
|
|
||||||
|
|
||||||
// shuffle
|
|
||||||
await this.shuffleArray(positions);
|
|
||||||
|
|
||||||
// build out the char sets
|
|
||||||
let allCharSet = "";
|
|
||||||
|
|
||||||
let lowercaseCharSet = "abcdefghijkmnopqrstuvwxyz";
|
|
||||||
if (o.ambiguous) {
|
|
||||||
lowercaseCharSet += "l";
|
|
||||||
}
|
|
||||||
if (o.lowercase) {
|
|
||||||
allCharSet += lowercaseCharSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let uppercaseCharSet = "ABCDEFGHJKLMNPQRSTUVWXYZ";
|
|
||||||
if (o.ambiguous) {
|
|
||||||
uppercaseCharSet += "IO";
|
|
||||||
}
|
|
||||||
if (o.uppercase) {
|
|
||||||
allCharSet += uppercaseCharSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let numberCharSet = "23456789";
|
|
||||||
if (o.ambiguous) {
|
|
||||||
numberCharSet += "01";
|
|
||||||
}
|
|
||||||
if (o.number) {
|
|
||||||
allCharSet += numberCharSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
const specialCharSet = "!@#$%^&*";
|
|
||||||
if (o.special) {
|
|
||||||
allCharSet += specialCharSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let password = "";
|
|
||||||
for (let i = 0; i < o.length; i++) {
|
|
||||||
let positionChars: string;
|
|
||||||
switch (positions[i]) {
|
|
||||||
case "l":
|
|
||||||
positionChars = lowercaseCharSet;
|
|
||||||
break;
|
|
||||||
case "u":
|
|
||||||
positionChars = uppercaseCharSet;
|
|
||||||
break;
|
|
||||||
case "n":
|
|
||||||
positionChars = numberCharSet;
|
|
||||||
break;
|
|
||||||
case "s":
|
|
||||||
positionChars = specialCharSet;
|
|
||||||
break;
|
|
||||||
case "a":
|
|
||||||
positionChars = allCharSet;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomCharIndex = await this.cryptoService.randomNumber(0, positionChars.length - 1);
|
|
||||||
password += positionChars.charAt(randomCharIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return password;
|
|
||||||
}
|
|
||||||
|
|
||||||
async generatePassphrase(options: any): Promise<string> {
|
|
||||||
const o = Object.assign({}, DefaultOptions, options);
|
|
||||||
|
|
||||||
if (o.numWords == null || o.numWords <= 2) {
|
|
||||||
o.numWords = DefaultOptions.numWords;
|
|
||||||
}
|
|
||||||
if (o.wordSeparator == null || o.wordSeparator.length === 0 || o.wordSeparator.length > 1) {
|
|
||||||
o.wordSeparator = " ";
|
|
||||||
}
|
|
||||||
if (o.capitalize == null) {
|
|
||||||
o.capitalize = false;
|
|
||||||
}
|
|
||||||
if (o.includeNumber == null) {
|
|
||||||
o.includeNumber = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const listLength = EEFLongWordList.length - 1;
|
|
||||||
const wordList = new Array(o.numWords);
|
|
||||||
for (let i = 0; i < o.numWords; i++) {
|
|
||||||
const wordIndex = await this.cryptoService.randomNumber(0, listLength);
|
|
||||||
if (o.capitalize) {
|
|
||||||
wordList[i] = this.capitalize(EEFLongWordList[wordIndex]);
|
|
||||||
} else {
|
|
||||||
wordList[i] = EEFLongWordList[wordIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (o.includeNumber) {
|
|
||||||
await this.appendRandomNumberToRandomWord(wordList);
|
|
||||||
}
|
|
||||||
return wordList.join(o.wordSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOptions(): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
|
||||||
let options = await this.stateService.getPasswordGenerationOptions();
|
|
||||||
if (options == null) {
|
|
||||||
options = Object.assign({}, DefaultOptions);
|
|
||||||
} else {
|
|
||||||
options = Object.assign({}, DefaultOptions, options);
|
|
||||||
}
|
|
||||||
await this.stateService.setPasswordGenerationOptions(options);
|
|
||||||
const enforcedOptions = await this.enforcePasswordGeneratorPoliciesOnOptions(options);
|
|
||||||
options = enforcedOptions[0];
|
|
||||||
return [options, enforcedOptions[1]];
|
|
||||||
}
|
|
||||||
|
|
||||||
async enforcePasswordGeneratorPoliciesOnOptions(
|
|
||||||
options: any,
|
|
||||||
): Promise<[any, PasswordGeneratorPolicyOptions]> {
|
|
||||||
let enforcedPolicyOptions = await this.getPasswordGeneratorPolicyOptions();
|
|
||||||
if (enforcedPolicyOptions != null) {
|
|
||||||
if (options.length < enforcedPolicyOptions.minLength) {
|
|
||||||
options.length = enforcedPolicyOptions.minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useUppercase) {
|
|
||||||
options.uppercase = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useLowercase) {
|
|
||||||
options.lowercase = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useNumbers) {
|
|
||||||
options.number = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
|
||||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.useSpecial) {
|
|
||||||
options.special = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
|
||||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Must normalize these fields because the receiving call expects all options to pass the current rules
|
|
||||||
if (options.minSpecial + options.minNumber > options.length) {
|
|
||||||
options.minSpecial = options.length - options.minNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
|
||||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.capitalize) {
|
|
||||||
options.capitalize = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.includeNumber) {
|
|
||||||
options.includeNumber = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force default type if password/passphrase selected via policy
|
|
||||||
if (
|
|
||||||
enforcedPolicyOptions.defaultType === "password" ||
|
|
||||||
enforcedPolicyOptions.defaultType === "passphrase"
|
|
||||||
) {
|
|
||||||
options.type = enforcedPolicyOptions.defaultType;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// UI layer expects an instantiated object to prevent more explicit null checks
|
|
||||||
enforcedPolicyOptions = new PasswordGeneratorPolicyOptions();
|
|
||||||
}
|
|
||||||
return [options, enforcedPolicyOptions];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPasswordGeneratorPolicyOptions(): Promise<PasswordGeneratorPolicyOptions> {
|
|
||||||
const policies: Policy[] =
|
|
||||||
this.policyService == null
|
|
||||||
? null
|
|
||||||
: await this.policyService.getAll(PolicyType.PasswordGenerator);
|
|
||||||
let enforcedOptions: PasswordGeneratorPolicyOptions = null;
|
|
||||||
|
|
||||||
if (policies == null || policies.length === 0) {
|
|
||||||
return enforcedOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
policies.forEach((currentPolicy) => {
|
|
||||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedOptions == null) {
|
|
||||||
enforcedOptions = new PasswordGeneratorPolicyOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Password wins in multi-org collisions
|
|
||||||
if (currentPolicy.data.defaultType != null && enforcedOptions.defaultType !== "password") {
|
|
||||||
enforcedOptions.defaultType = currentPolicy.data.defaultType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPolicy.data.minLength != null &&
|
|
||||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
|
||||||
) {
|
|
||||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.useUpper) {
|
|
||||||
enforcedOptions.useUppercase = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.useLower) {
|
|
||||||
enforcedOptions.useLowercase = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.useNumbers) {
|
|
||||||
enforcedOptions.useNumbers = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPolicy.data.minNumbers != null &&
|
|
||||||
currentPolicy.data.minNumbers > enforcedOptions.numberCount
|
|
||||||
) {
|
|
||||||
enforcedOptions.numberCount = currentPolicy.data.minNumbers;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.useSpecial) {
|
|
||||||
enforcedOptions.useSpecial = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPolicy.data.minSpecial != null &&
|
|
||||||
currentPolicy.data.minSpecial > enforcedOptions.specialCount
|
|
||||||
) {
|
|
||||||
enforcedOptions.specialCount = currentPolicy.data.minSpecial;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPolicy.data.minNumberWords != null &&
|
|
||||||
currentPolicy.data.minNumberWords > enforcedOptions.minNumberWords
|
|
||||||
) {
|
|
||||||
enforcedOptions.minNumberWords = currentPolicy.data.minNumberWords;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.capitalize) {
|
|
||||||
enforcedOptions.capitalize = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.includeNumber) {
|
|
||||||
enforcedOptions.includeNumber = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return enforcedOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveOptions(options: any) {
|
|
||||||
await this.stateService.setPasswordGenerationOptions(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getHistory(): Promise<GeneratedPasswordHistory[]> {
|
|
||||||
const hasKey = await this.cryptoService.hasKey();
|
|
||||||
if (!hasKey) {
|
|
||||||
return new Array<GeneratedPasswordHistory>();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((await this.stateService.getDecryptedPasswordGenerationHistory()) == null) {
|
|
||||||
const encrypted = await this.stateService.getEncryptedPasswordGenerationHistory();
|
|
||||||
const decrypted = await this.decryptHistory(encrypted);
|
|
||||||
await this.stateService.setDecryptedPasswordGenerationHistory(decrypted);
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordGenerationHistory =
|
|
||||||
await this.stateService.getDecryptedPasswordGenerationHistory();
|
|
||||||
return passwordGenerationHistory != null
|
|
||||||
? passwordGenerationHistory
|
|
||||||
: new Array<GeneratedPasswordHistory>();
|
|
||||||
}
|
|
||||||
|
|
||||||
async addHistory(password: string): Promise<any> {
|
|
||||||
// Cannot add new history if no key is available
|
|
||||||
const hasKey = await this.cryptoService.hasKey();
|
|
||||||
if (!hasKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentHistory = await this.getHistory();
|
|
||||||
|
|
||||||
// Prevent duplicates
|
|
||||||
if (this.matchesPrevious(password, currentHistory)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentHistory.unshift(new GeneratedPasswordHistory(password, Date.now()));
|
|
||||||
|
|
||||||
// Remove old items.
|
|
||||||
if (currentHistory.length > MaxPasswordsInHistory) {
|
|
||||||
currentHistory.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
const newHistory = await this.encryptHistory(currentHistory);
|
|
||||||
return await this.stateService.setEncryptedPasswordGenerationHistory(newHistory);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clear(userId?: string): Promise<any> {
|
|
||||||
await this.stateService.setEncryptedPasswordGenerationHistory(null, { userId: userId });
|
|
||||||
await this.stateService.setDecryptedPasswordGenerationHistory(null, { userId: userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordStrength(password: string, userInputs: string[] = null): zxcvbn.ZXCVBNResult {
|
|
||||||
if (password == null || password.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
let globalUserInputs = ["bitwarden", "bit", "warden"];
|
|
||||||
if (userInputs != null && userInputs.length > 0) {
|
|
||||||
globalUserInputs = globalUserInputs.concat(userInputs);
|
|
||||||
}
|
|
||||||
// Use a hash set to get rid of any duplicate user inputs
|
|
||||||
const finalUserInputs = Array.from(new Set(globalUserInputs));
|
|
||||||
const result = zxcvbn(password, finalUserInputs);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
normalizeOptions(options: any, enforcedPolicyOptions: PasswordGeneratorPolicyOptions) {
|
|
||||||
options.minLowercase = 0;
|
|
||||||
options.minUppercase = 0;
|
|
||||||
|
|
||||||
if (!options.length || options.length < 5) {
|
|
||||||
options.length = 5;
|
|
||||||
} else if (options.length > 128) {
|
|
||||||
options.length = 128;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.length < enforcedPolicyOptions.minLength) {
|
|
||||||
options.length = enforcedPolicyOptions.minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.minNumber) {
|
|
||||||
options.minNumber = 0;
|
|
||||||
} else if (options.minNumber > options.length) {
|
|
||||||
options.minNumber = options.length;
|
|
||||||
} else if (options.minNumber > 9) {
|
|
||||||
options.minNumber = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minNumber < enforcedPolicyOptions.numberCount) {
|
|
||||||
options.minNumber = enforcedPolicyOptions.numberCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!options.minSpecial) {
|
|
||||||
options.minSpecial = 0;
|
|
||||||
} else if (options.minSpecial > options.length) {
|
|
||||||
options.minSpecial = options.length;
|
|
||||||
} else if (options.minSpecial > 9) {
|
|
||||||
options.minSpecial = 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minSpecial < enforcedPolicyOptions.specialCount) {
|
|
||||||
options.minSpecial = enforcedPolicyOptions.specialCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.minSpecial + options.minNumber > options.length) {
|
|
||||||
options.minSpecial = options.length - options.minNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.numWords == null || options.length < 3) {
|
|
||||||
options.numWords = 3;
|
|
||||||
} else if (options.numWords > 20) {
|
|
||||||
options.numWords = 20;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.numWords < enforcedPolicyOptions.minNumberWords) {
|
|
||||||
options.numWords = enforcedPolicyOptions.minNumberWords;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.wordSeparator != null && options.wordSeparator.length > 1) {
|
|
||||||
options.wordSeparator = options.wordSeparator[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sanitizePasswordLength(options, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private capitalize(str: string) {
|
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async appendRandomNumberToRandomWord(wordList: string[]) {
|
|
||||||
if (wordList == null || wordList.length <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const index = await this.cryptoService.randomNumber(0, wordList.length - 1);
|
|
||||||
const num = await this.cryptoService.randomNumber(0, 9);
|
|
||||||
wordList[index] = wordList[index] + num;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async encryptHistory(
|
|
||||||
history: GeneratedPasswordHistory[],
|
|
||||||
): Promise<GeneratedPasswordHistory[]> {
|
|
||||||
if (history == null || history.length === 0) {
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = history.map(async (item) => {
|
|
||||||
const encrypted = await this.cryptoService.encrypt(item.password);
|
|
||||||
return new GeneratedPasswordHistory(encrypted.encryptedString, item.date);
|
|
||||||
});
|
|
||||||
|
|
||||||
return await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async decryptHistory(
|
|
||||||
history: GeneratedPasswordHistory[],
|
|
||||||
): Promise<GeneratedPasswordHistory[]> {
|
|
||||||
if (history == null || history.length === 0) {
|
|
||||||
return Promise.resolve([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const promises = history.map(async (item) => {
|
|
||||||
const decrypted = await this.cryptoService.decryptToUtf8(new EncString(item.password));
|
|
||||||
return new GeneratedPasswordHistory(decrypted, item.date);
|
|
||||||
});
|
|
||||||
|
|
||||||
return await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
private matchesPrevious(password: string, history: GeneratedPasswordHistory[]): boolean {
|
|
||||||
if (history == null || history.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return history[history.length - 1].password === password;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ref: https://stackoverflow.com/a/12646864/1090359
|
|
||||||
private async shuffleArray(array: string[]) {
|
|
||||||
for (let i = array.length - 1; i > 0; i--) {
|
|
||||||
const j = await this.cryptoService.randomNumber(0, i);
|
|
||||||
[array[i], array[j]] = [array[j], array[i]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sanitizePasswordLength(options: any, forGeneration: boolean) {
|
|
||||||
let minUppercaseCalc = 0;
|
|
||||||
let minLowercaseCalc = 0;
|
|
||||||
let minNumberCalc: number = options.minNumber;
|
|
||||||
let minSpecialCalc: number = options.minSpecial;
|
|
||||||
|
|
||||||
if (options.uppercase && options.minUppercase <= 0) {
|
|
||||||
minUppercaseCalc = 1;
|
|
||||||
} else if (!options.uppercase) {
|
|
||||||
minUppercaseCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.lowercase && options.minLowercase <= 0) {
|
|
||||||
minLowercaseCalc = 1;
|
|
||||||
} else if (!options.lowercase) {
|
|
||||||
minLowercaseCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.number && options.minNumber <= 0) {
|
|
||||||
minNumberCalc = 1;
|
|
||||||
} else if (!options.number) {
|
|
||||||
minNumberCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.special && options.minSpecial <= 0) {
|
|
||||||
minSpecialCalc = 1;
|
|
||||||
} else if (!options.special) {
|
|
||||||
minSpecialCalc = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should never happen but is a final safety net
|
|
||||||
if (!options.length || options.length < 1) {
|
|
||||||
options.length = 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
const minLength: number = minUppercaseCalc + minLowercaseCalc + minNumberCalc + minSpecialCalc;
|
|
||||||
// Normalize and Generation both require this modification
|
|
||||||
if (options.length < minLength) {
|
|
||||||
options.length = minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply other changes if the options object passed in is for generation
|
|
||||||
if (forGeneration) {
|
|
||||||
options.minUppercase = minUppercaseCalc;
|
|
||||||
options.minLowercase = minLowercaseCalc;
|
|
||||||
options.minNumber = minNumberCalc;
|
|
||||||
options.minSpecial = minSpecialCalc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
import { ApiService } from "../abstractions/api.service";
|
|
||||||
import { OrganizationService } from "../abstractions/organization.service";
|
|
||||||
import { PolicyService as PolicyServiceAbstraction } from "../abstractions/policy.service";
|
|
||||||
import { StateService } from "../abstractions/state.service";
|
|
||||||
import { OrganizationUserStatusType } from "../enums/organizationUserStatusType";
|
|
||||||
import { OrganizationUserType } from "../enums/organizationUserType";
|
|
||||||
import { PolicyType } from "../enums/policyType";
|
|
||||||
import { PolicyData } from "../models/data/policyData";
|
|
||||||
import { MasterPasswordPolicyOptions } from "../models/domain/masterPasswordPolicyOptions";
|
|
||||||
import { Organization } from "../models/domain/organization";
|
|
||||||
import { Policy } from "../models/domain/policy";
|
|
||||||
import { ResetPasswordPolicyOptions } from "../models/domain/resetPasswordPolicyOptions";
|
|
||||||
import { ListResponse } from "../models/response/listResponse";
|
|
||||||
import { PolicyResponse } from "../models/response/policyResponse";
|
|
||||||
|
|
||||||
export class PolicyService implements PolicyServiceAbstraction {
|
|
||||||
policyCache: Policy[];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private stateService: StateService,
|
|
||||||
private organizationService: OrganizationService,
|
|
||||||
private apiService: ApiService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
async clearCache(): Promise<void> {
|
|
||||||
await this.stateService.setDecryptedPolicies(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAll(type?: PolicyType, userId?: string): Promise<Policy[]> {
|
|
||||||
let response: Policy[] = [];
|
|
||||||
const decryptedPolicies = await this.stateService.getDecryptedPolicies({ userId: userId });
|
|
||||||
if (decryptedPolicies != null) {
|
|
||||||
response = decryptedPolicies;
|
|
||||||
} else {
|
|
||||||
const diskPolicies = await this.stateService.getEncryptedPolicies({ userId: userId });
|
|
||||||
for (const id in diskPolicies) {
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (diskPolicies.hasOwnProperty(id)) {
|
|
||||||
response.push(new Policy(diskPolicies[id]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await this.stateService.setDecryptedPolicies(response, { userId: userId });
|
|
||||||
}
|
|
||||||
if (type != null) {
|
|
||||||
return response.filter((policy) => policy.type === type);
|
|
||||||
} else {
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getPolicyForOrganization(policyType: PolicyType, organizationId: string): Promise<Policy> {
|
|
||||||
const org = await this.organizationService.get(organizationId);
|
|
||||||
if (org?.isProviderUser) {
|
|
||||||
const orgPolicies = await this.apiService.getPolicies(organizationId);
|
|
||||||
const policy = orgPolicies.data.find((p) => p.organizationId === organizationId);
|
|
||||||
|
|
||||||
if (policy == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Policy(new PolicyData(policy));
|
|
||||||
}
|
|
||||||
|
|
||||||
const policies = await this.getAll(policyType);
|
|
||||||
return policies.find((p) => p.organizationId === organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
async replace(policies: { [id: string]: PolicyData }): Promise<any> {
|
|
||||||
await this.stateService.setDecryptedPolicies(null);
|
|
||||||
await this.stateService.setEncryptedPolicies(policies);
|
|
||||||
}
|
|
||||||
|
|
||||||
async clear(userId?: string): Promise<any> {
|
|
||||||
await this.stateService.setDecryptedPolicies(null, { userId: userId });
|
|
||||||
await this.stateService.setEncryptedPolicies(null, { userId: userId });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMasterPasswordPoliciesForInvitedUsers(
|
|
||||||
orgId: string,
|
|
||||||
): Promise<MasterPasswordPolicyOptions> {
|
|
||||||
const userId = await this.stateService.getUserId();
|
|
||||||
const response = await this.apiService.getPoliciesByInvitedUser(orgId, userId);
|
|
||||||
const policies = await this.mapPoliciesFromToken(response);
|
|
||||||
return this.getMasterPasswordPolicyOptions(policies);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMasterPasswordPolicyOptions(policies?: Policy[]): Promise<MasterPasswordPolicyOptions> {
|
|
||||||
let enforcedOptions: MasterPasswordPolicyOptions = null;
|
|
||||||
|
|
||||||
if (policies == null) {
|
|
||||||
policies = await this.getAll(PolicyType.MasterPassword);
|
|
||||||
} else {
|
|
||||||
policies = policies.filter((p) => p.type === PolicyType.MasterPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (policies == null || policies.length === 0) {
|
|
||||||
return enforcedOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
policies.forEach((currentPolicy) => {
|
|
||||||
if (!currentPolicy.enabled || currentPolicy.data == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedOptions == null) {
|
|
||||||
enforcedOptions = new MasterPasswordPolicyOptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPolicy.data.minComplexity != null &&
|
|
||||||
currentPolicy.data.minComplexity > enforcedOptions.minComplexity
|
|
||||||
) {
|
|
||||||
enforcedOptions.minComplexity = currentPolicy.data.minComplexity;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPolicy.data.minLength != null &&
|
|
||||||
currentPolicy.data.minLength > enforcedOptions.minLength
|
|
||||||
) {
|
|
||||||
enforcedOptions.minLength = currentPolicy.data.minLength;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.requireUpper) {
|
|
||||||
enforcedOptions.requireUpper = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.requireLower) {
|
|
||||||
enforcedOptions.requireLower = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.requireNumbers) {
|
|
||||||
enforcedOptions.requireNumbers = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPolicy.data.requireSpecial) {
|
|
||||||
enforcedOptions.requireSpecial = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return enforcedOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
evaluateMasterPassword(
|
|
||||||
passwordStrength: number,
|
|
||||||
newPassword: string,
|
|
||||||
enforcedPolicyOptions: MasterPasswordPolicyOptions,
|
|
||||||
): boolean {
|
|
||||||
if (enforcedPolicyOptions == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
enforcedPolicyOptions.minComplexity > 0 &&
|
|
||||||
enforcedPolicyOptions.minComplexity > passwordStrength
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
enforcedPolicyOptions.minLength > 0 &&
|
|
||||||
enforcedPolicyOptions.minLength > newPassword.length
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.requireUpper && newPassword.toLocaleLowerCase() === newPassword) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.requireLower && newPassword.toLocaleUpperCase() === newPassword) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (enforcedPolicyOptions.requireNumbers && !/[0-9]/.test(newPassword)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
if (enforcedPolicyOptions.requireSpecial && !/[!@#$%\^&*]/g.test(newPassword)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
getResetPasswordPolicyOptions(
|
|
||||||
policies: Policy[],
|
|
||||||
orgId: string,
|
|
||||||
): [ResetPasswordPolicyOptions, boolean] {
|
|
||||||
const resetPasswordPolicyOptions = new ResetPasswordPolicyOptions();
|
|
||||||
|
|
||||||
if (policies == null || orgId == null) {
|
|
||||||
return [resetPasswordPolicyOptions, false];
|
|
||||||
}
|
|
||||||
|
|
||||||
const policy = policies.find(
|
|
||||||
(p) => p.organizationId === orgId && p.type === PolicyType.ResetPassword && p.enabled,
|
|
||||||
);
|
|
||||||
resetPasswordPolicyOptions.autoEnrollEnabled = policy?.data?.autoEnrollEnabled ?? false;
|
|
||||||
|
|
||||||
return [resetPasswordPolicyOptions, policy?.enabled ?? false];
|
|
||||||
}
|
|
||||||
|
|
||||||
mapPoliciesFromToken(policiesResponse: ListResponse<PolicyResponse>): Policy[] {
|
|
||||||
if (policiesResponse == null || policiesResponse.data == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const policiesData = policiesResponse.data.map((p) => new PolicyData(p));
|
|
||||||
return policiesData.map((p) => new Policy(p));
|
|
||||||
}
|
|
||||||
|
|
||||||
async policyAppliesToUser(
|
|
||||||
policyType: PolicyType,
|
|
||||||
policyFilter?: (policy: Policy) => boolean,
|
|
||||||
userId?: string,
|
|
||||||
) {
|
|
||||||
const policies = await this.getAll(policyType, userId);
|
|
||||||
const organizations = await this.organizationService.getAll(userId);
|
|
||||||
let filteredPolicies;
|
|
||||||
|
|
||||||
if (policyFilter != null) {
|
|
||||||
filteredPolicies = policies.filter((p) => p.enabled && policyFilter(p));
|
|
||||||
} else {
|
|
||||||
filteredPolicies = policies.filter((p) => p.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
const policySet = new Set(filteredPolicies.map((p) => p.organizationId));
|
|
||||||
|
|
||||||
return organizations.some(
|
|
||||||
(o) =>
|
|
||||||
o.enabled &&
|
|
||||||
o.status >= OrganizationUserStatusType.Accepted &&
|
|
||||||
o.usePolicies &&
|
|
||||||
!this.isExcemptFromPolicies(o, policyType) &&
|
|
||||||
policySet.has(o.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private isExcemptFromPolicies(organization: Organization, policyType: PolicyType) {
|
|
||||||
if (policyType === PolicyType.MaximumVaultTimeout) {
|
|
||||||
return organization.type === OrganizationUserType.Owner;
|
|
||||||
}
|
|
||||||
|
|
||||||
return organization.isExemptFromPolicies;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,7 @@ export class WindowMain {
|
|||||||
win: BrowserWindow;
|
win: BrowserWindow;
|
||||||
isQuitting = false;
|
isQuitting = false;
|
||||||
|
|
||||||
private windowStateChangeTimer: NodeJS.Timer;
|
private windowStateChangeTimer: NodeJS.Timeout;
|
||||||
private windowStates: { [key: string]: any } = {};
|
private windowStates: { [key: string]: any } = {};
|
||||||
private enableAlwaysOnTop = false;
|
private enableAlwaysOnTop = false;
|
||||||
|
|
||||||
|
|||||||
@@ -1,624 +0,0 @@
|
|||||||
import * as http from "http";
|
|
||||||
|
|
||||||
import * as program from "commander";
|
|
||||||
import * as inquirer from "inquirer";
|
|
||||||
import Separator from "inquirer/lib/objects/separator";
|
|
||||||
|
|
||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
|
||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
|
||||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
|
||||||
import { PasswordGenerationService } from "@/jslib/common/src/abstractions/passwordGeneration.service";
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
|
||||||
import { PolicyService } from "@/jslib/common/src/abstractions/policy.service";
|
|
||||||
import { StateService } from "@/jslib/common/src/abstractions/state.service";
|
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
|
|
||||||
import { NodeUtils } from "@/jslib/common/src/misc/nodeUtils";
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
|
||||||
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
|
|
||||||
import {
|
|
||||||
ApiLogInCredentials,
|
|
||||||
PasswordLogInCredentials,
|
|
||||||
SsoLogInCredentials,
|
|
||||||
} from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
|
|
||||||
import { TwoFactorEmailRequest } from "@/jslib/common/src/models/request/twoFactorEmailRequest";
|
|
||||||
import { UpdateTempPasswordRequest } from "@/jslib/common/src/models/request/updateTempPasswordRequest";
|
|
||||||
import { ErrorResponse } from "@/jslib/common/src/models/response/errorResponse";
|
|
||||||
|
|
||||||
import { Response } from "../models/response";
|
|
||||||
import { MessageResponse } from "../models/response/messageResponse";
|
|
||||||
|
|
||||||
export class LoginCommand {
|
|
||||||
protected validatedParams: () => Promise<any>;
|
|
||||||
protected success: () => Promise<MessageResponse>;
|
|
||||||
protected logout: () => Promise<void>;
|
|
||||||
protected canInteract: boolean;
|
|
||||||
protected clientId: string;
|
|
||||||
protected clientSecret: string;
|
|
||||||
protected email: string;
|
|
||||||
|
|
||||||
private ssoRedirectUri: string = null;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected authService: AuthService,
|
|
||||||
protected apiService: ApiService,
|
|
||||||
protected i18nService: I18nService,
|
|
||||||
protected environmentService: EnvironmentService,
|
|
||||||
protected passwordGenerationService: PasswordGenerationService,
|
|
||||||
protected cryptoFunctionService: CryptoFunctionService,
|
|
||||||
protected platformUtilsService: PlatformUtilsService,
|
|
||||||
protected stateService: StateService,
|
|
||||||
protected cryptoService: CryptoService,
|
|
||||||
protected policyService: PolicyService,
|
|
||||||
protected twoFactorService: TwoFactorService,
|
|
||||||
clientId: string,
|
|
||||||
) {
|
|
||||||
this.clientId = clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(email: string, password: string, options: program.OptionValues) {
|
|
||||||
this.canInteract = process.env.BW_NOINTERACTION !== "true";
|
|
||||||
|
|
||||||
let ssoCodeVerifier: string = null;
|
|
||||||
let ssoCode: string = null;
|
|
||||||
let orgIdentifier: string = null;
|
|
||||||
|
|
||||||
let clientId: string = null;
|
|
||||||
let clientSecret: string = null;
|
|
||||||
|
|
||||||
let selectedProvider: any = null;
|
|
||||||
|
|
||||||
if (options.apikey != null) {
|
|
||||||
const apiIdentifiers = await this.apiIdentifiers();
|
|
||||||
clientId = apiIdentifiers.clientId;
|
|
||||||
clientSecret = apiIdentifiers.clientSecret;
|
|
||||||
} else if (options.sso != null && this.canInteract) {
|
|
||||||
const passwordOptions: any = {
|
|
||||||
type: "password",
|
|
||||||
length: 64,
|
|
||||||
uppercase: true,
|
|
||||||
lowercase: true,
|
|
||||||
numbers: true,
|
|
||||||
special: false,
|
|
||||||
};
|
|
||||||
const state = await this.passwordGenerationService.generatePassword(passwordOptions);
|
|
||||||
ssoCodeVerifier = await this.passwordGenerationService.generatePassword(passwordOptions);
|
|
||||||
const codeVerifierHash = await this.cryptoFunctionService.hash(ssoCodeVerifier, "sha256");
|
|
||||||
const codeChallenge = Utils.fromBufferToUrlB64(codeVerifierHash);
|
|
||||||
try {
|
|
||||||
const ssoParams = await this.openSsoPrompt(codeChallenge, state);
|
|
||||||
ssoCode = ssoParams.ssoCode;
|
|
||||||
orgIdentifier = ssoParams.orgIdentifier;
|
|
||||||
} catch {
|
|
||||||
return Response.badRequest("Something went wrong. Try again.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if ((email == null || email === "") && this.canInteract) {
|
|
||||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
|
||||||
output: process.stderr,
|
|
||||||
})({
|
|
||||||
type: "input",
|
|
||||||
name: "email",
|
|
||||||
message: "Email address:",
|
|
||||||
});
|
|
||||||
email = answer.email;
|
|
||||||
}
|
|
||||||
if (email == null || email.trim() === "") {
|
|
||||||
return Response.badRequest("Email address is required.");
|
|
||||||
}
|
|
||||||
if (email.indexOf("@") === -1) {
|
|
||||||
return Response.badRequest("Email address is invalid.");
|
|
||||||
}
|
|
||||||
this.email = email;
|
|
||||||
|
|
||||||
if (password == null || password === "") {
|
|
||||||
if (options.passwordfile) {
|
|
||||||
password = await NodeUtils.readFirstLine(options.passwordfile);
|
|
||||||
} else if (options.passwordenv && process.env[options.passwordenv]) {
|
|
||||||
password = process.env[options.passwordenv];
|
|
||||||
} else if (this.canInteract) {
|
|
||||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
|
||||||
output: process.stderr,
|
|
||||||
})({
|
|
||||||
type: "password",
|
|
||||||
name: "password",
|
|
||||||
message: "Master password:",
|
|
||||||
});
|
|
||||||
password = answer.password;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password == null || password === "") {
|
|
||||||
return Response.badRequest("Master password is required.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let twoFactorToken: string = options.code;
|
|
||||||
let twoFactorMethod: TwoFactorProviderType = null;
|
|
||||||
try {
|
|
||||||
if (options.method != null) {
|
|
||||||
twoFactorMethod = parseInt(options.method, null);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return Response.error("Invalid two-step login method.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const twoFactor =
|
|
||||||
twoFactorToken == null
|
|
||||||
? null
|
|
||||||
: new TokenRequestTwoFactor(twoFactorMethod, twoFactorToken, false);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (this.validatedParams != null) {
|
|
||||||
await this.validatedParams();
|
|
||||||
}
|
|
||||||
|
|
||||||
let response: AuthResult = null;
|
|
||||||
if (clientId != null && clientSecret != null) {
|
|
||||||
response = await this.authService.logIn(new ApiLogInCredentials(clientId, clientSecret));
|
|
||||||
} else if (ssoCode != null && ssoCodeVerifier != null) {
|
|
||||||
response = await this.authService.logIn(
|
|
||||||
new SsoLogInCredentials(
|
|
||||||
ssoCode,
|
|
||||||
ssoCodeVerifier,
|
|
||||||
this.ssoRedirectUri,
|
|
||||||
orgIdentifier,
|
|
||||||
twoFactor,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
response = await this.authService.logIn(
|
|
||||||
new PasswordLogInCredentials(email, password, null, twoFactor),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (response.captchaSiteKey) {
|
|
||||||
const credentials = new PasswordLogInCredentials(email, password);
|
|
||||||
const handledResponse = await this.handleCaptchaRequired(twoFactor, credentials);
|
|
||||||
|
|
||||||
// Error Response
|
|
||||||
if (handledResponse instanceof Response) {
|
|
||||||
return handledResponse;
|
|
||||||
} else {
|
|
||||||
response = handledResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (response.requiresTwoFactor) {
|
|
||||||
const twoFactorProviders = this.twoFactorService.getSupportedProviders(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: (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(
|
|
||||||
new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken),
|
|
||||||
null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.captchaSiteKey) {
|
|
||||||
const twoFactorRequest = new TokenRequestTwoFactor(selectedProvider.type, twoFactorToken);
|
|
||||||
const handledResponse = await this.handleCaptchaRequired(twoFactorRequest);
|
|
||||||
|
|
||||||
// Error Response
|
|
||||||
if (handledResponse instanceof Response) {
|
|
||||||
return handledResponse;
|
|
||||||
} else {
|
|
||||||
response = handledResponse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.requiresTwoFactor) {
|
|
||||||
return Response.error("Login failed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.resetMasterPassword) {
|
|
||||||
return Response.error(
|
|
||||||
"In order to log in with SSO from the CLI, you must first log in" +
|
|
||||||
" through the web vault to set your master password.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle Updating Temp Password if NOT using an API Key for authentication
|
|
||||||
if (response.forcePasswordReset && clientId == null && clientSecret == null) {
|
|
||||||
return await this.updateTempPassword();
|
|
||||||
}
|
|
||||||
|
|
||||||
return await this.handleSuccessResponse();
|
|
||||||
} catch (e) {
|
|
||||||
return Response.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleSuccessResponse(): Promise<Response> {
|
|
||||||
if (this.success != null) {
|
|
||||||
const res = await this.success();
|
|
||||||
return Response.success(res);
|
|
||||||
} else {
|
|
||||||
const res = new MessageResponse("You are logged in!", null);
|
|
||||||
return Response.success(res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async updateTempPassword(error?: string): Promise<Response> {
|
|
||||||
// If no interaction available, alert user to use web vault
|
|
||||||
if (!this.canInteract) {
|
|
||||||
await this.logout();
|
|
||||||
this.authService.logOut(() => {
|
|
||||||
/* Do nothing */
|
|
||||||
});
|
|
||||||
return Response.error(
|
|
||||||
new MessageResponse(
|
|
||||||
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now via the web vault. You have been logged out.",
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.email == null || this.email === "undefined") {
|
|
||||||
this.email = await this.stateService.getEmail();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get New Master Password
|
|
||||||
const baseMessage =
|
|
||||||
"An organization administrator recently changed your master password. In order to access the vault, you must update your master password now.\n" +
|
|
||||||
"Master password: ";
|
|
||||||
const firstMessage = error != null ? error + baseMessage : baseMessage;
|
|
||||||
const mp: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
|
||||||
type: "password",
|
|
||||||
name: "password",
|
|
||||||
message: firstMessage,
|
|
||||||
});
|
|
||||||
const masterPassword = mp.password;
|
|
||||||
|
|
||||||
// Master Password Validation
|
|
||||||
if (masterPassword == null || masterPassword === "") {
|
|
||||||
return this.updateTempPassword("Master password is required.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (masterPassword.length < 8) {
|
|
||||||
return this.updateTempPassword("Master password must be at least 8 characters long.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strength & Policy Validation
|
|
||||||
const strengthResult = this.passwordGenerationService.passwordStrength(
|
|
||||||
masterPassword,
|
|
||||||
this.getPasswordStrengthUserInput(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get New Master Password Re-type
|
|
||||||
const reTypeMessage = "Re-type New Master password (Strength: " + strengthResult.score + ")";
|
|
||||||
const retype: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
|
||||||
type: "password",
|
|
||||||
name: "password",
|
|
||||||
message: reTypeMessage,
|
|
||||||
});
|
|
||||||
const masterPasswordRetype = retype.password;
|
|
||||||
|
|
||||||
// Re-type Validation
|
|
||||||
if (masterPassword !== masterPasswordRetype) {
|
|
||||||
return this.updateTempPassword("Master password confirmation does not match.\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Hint (optional)
|
|
||||||
const hint: inquirer.Answers = await inquirer.createPromptModule({ output: process.stderr })({
|
|
||||||
type: "input",
|
|
||||||
name: "input",
|
|
||||||
message: "Master Password Hint (optional):",
|
|
||||||
});
|
|
||||||
const masterPasswordHint = hint.input;
|
|
||||||
|
|
||||||
// Retrieve details for key generation
|
|
||||||
const enforcedPolicyOptions = await this.policyService.getMasterPasswordPolicyOptions();
|
|
||||||
const kdf = await this.stateService.getKdfType();
|
|
||||||
const kdfIterations = await this.stateService.getKdfIterations();
|
|
||||||
|
|
||||||
if (
|
|
||||||
enforcedPolicyOptions != null &&
|
|
||||||
!this.policyService.evaluateMasterPassword(
|
|
||||||
strengthResult.score,
|
|
||||||
masterPassword,
|
|
||||||
enforcedPolicyOptions,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return this.updateTempPassword(
|
|
||||||
"Your new master password does not meet the policy requirements.\n",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Create new key and hash new password
|
|
||||||
const newKey = await this.cryptoService.makeKey(
|
|
||||||
masterPassword,
|
|
||||||
this.email.trim().toLowerCase(),
|
|
||||||
kdf,
|
|
||||||
kdfIterations,
|
|
||||||
);
|
|
||||||
const newPasswordHash = await this.cryptoService.hashPassword(masterPassword, newKey);
|
|
||||||
|
|
||||||
// Grab user's current enc key
|
|
||||||
const userEncKey = await this.cryptoService.getEncKey();
|
|
||||||
|
|
||||||
// Create new encKey for the User
|
|
||||||
const newEncKey = await this.cryptoService.remakeEncKey(newKey, userEncKey);
|
|
||||||
|
|
||||||
// Create request
|
|
||||||
const request = new UpdateTempPasswordRequest();
|
|
||||||
request.key = newEncKey[1].encryptedString;
|
|
||||||
request.newMasterPasswordHash = newPasswordHash;
|
|
||||||
request.masterPasswordHint = masterPasswordHint;
|
|
||||||
|
|
||||||
// Update user's password
|
|
||||||
await this.apiService.putUpdateTempPassword(request);
|
|
||||||
return this.handleSuccessResponse();
|
|
||||||
} catch (e) {
|
|
||||||
await this.logout();
|
|
||||||
this.authService.logOut(() => {
|
|
||||||
/* Do nothing */
|
|
||||||
});
|
|
||||||
return Response.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async handleCaptchaRequired(
|
|
||||||
twoFactorRequest: TokenRequestTwoFactor,
|
|
||||||
credentials: PasswordLogInCredentials = null,
|
|
||||||
): Promise<AuthResult | Response> {
|
|
||||||
const badCaptcha = Response.badRequest(
|
|
||||||
"Your authentication request has been flagged and will require user interaction to proceed.\n" +
|
|
||||||
"Please use your API key to validate this request and ensure BW_CLIENTSECRET is correct, if set.\n" +
|
|
||||||
"(https://bitwarden.com/help/cli-auth-challenges)",
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const captchaClientSecret = await this.apiClientSecret(true);
|
|
||||||
if (Utils.isNullOrWhitespace(captchaClientSecret)) {
|
|
||||||
return badCaptcha;
|
|
||||||
}
|
|
||||||
|
|
||||||
let authResultResponse: AuthResult = null;
|
|
||||||
if (credentials != null) {
|
|
||||||
credentials.captchaToken = captchaClientSecret;
|
|
||||||
credentials.twoFactor = twoFactorRequest;
|
|
||||||
authResultResponse = await this.authService.logIn(credentials);
|
|
||||||
} else {
|
|
||||||
authResultResponse = await this.authService.logInTwoFactor(
|
|
||||||
twoFactorRequest,
|
|
||||||
captchaClientSecret,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return authResultResponse;
|
|
||||||
} catch (e) {
|
|
||||||
if (
|
|
||||||
e instanceof ErrorResponse ||
|
|
||||||
(e.constructor.name === ErrorResponse.name &&
|
|
||||||
(e as ErrorResponse).message.includes("Captcha is invalid"))
|
|
||||||
) {
|
|
||||||
return badCaptcha;
|
|
||||||
} else {
|
|
||||||
return Response.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPasswordStrengthUserInput() {
|
|
||||||
let userInput: string[] = [];
|
|
||||||
const atPosition = this.email.indexOf("@");
|
|
||||||
if (atPosition > -1) {
|
|
||||||
userInput = userInput.concat(
|
|
||||||
this.email
|
|
||||||
.substr(0, atPosition)
|
|
||||||
.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.split(/[^A-Za-z0-9]/),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return userInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async apiClientId(): Promise<string> {
|
|
||||||
let clientId: string = null;
|
|
||||||
|
|
||||||
const storedClientId: string = process.env.BW_CLIENTID;
|
|
||||||
if (storedClientId == null) {
|
|
||||||
if (this.canInteract) {
|
|
||||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
|
||||||
output: process.stderr,
|
|
||||||
})({
|
|
||||||
type: "input",
|
|
||||||
name: "clientId",
|
|
||||||
message: "client_id:",
|
|
||||||
});
|
|
||||||
clientId = answer.clientId;
|
|
||||||
} else {
|
|
||||||
clientId = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
clientId = storedClientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async apiClientSecret(isAdditionalAuthentication = false): Promise<string> {
|
|
||||||
const additionalAuthenticationMessage = "Additional authentication required.\nAPI key ";
|
|
||||||
let clientSecret: string = null;
|
|
||||||
|
|
||||||
const storedClientSecret: string = this.clientSecret || process.env.BW_CLIENTSECRET;
|
|
||||||
if (this.canInteract && storedClientSecret == null) {
|
|
||||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
|
||||||
output: process.stderr,
|
|
||||||
})({
|
|
||||||
type: "input",
|
|
||||||
name: "clientSecret",
|
|
||||||
message:
|
|
||||||
(isAdditionalAuthentication ? additionalAuthenticationMessage : "") + "client_secret:",
|
|
||||||
});
|
|
||||||
clientSecret = answer.clientSecret;
|
|
||||||
} else {
|
|
||||||
clientSecret = storedClientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
return clientSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> {
|
|
||||||
return {
|
|
||||||
clientId: await this.apiClientId(),
|
|
||||||
clientSecret: await this.apiClientSecret(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async openSsoPrompt(
|
|
||||||
codeChallenge: string,
|
|
||||||
state: string,
|
|
||||||
): Promise<{ ssoCode: string; orgIdentifier: string }> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const callbackServer = http.createServer((req, res) => {
|
|
||||||
const urlString = "http://localhost" + req.url;
|
|
||||||
const url = new URL(urlString);
|
|
||||||
const code = url.searchParams.get("code");
|
|
||||||
const receivedState = url.searchParams.get("state");
|
|
||||||
const orgIdentifier = this.getOrgIdentifierFromState(receivedState);
|
|
||||||
res.setHeader("Content-Type", "text/html");
|
|
||||||
if (code != null && receivedState != null && this.checkState(receivedState, state)) {
|
|
||||||
res.writeHead(200);
|
|
||||||
res.end(
|
|
||||||
"<html><head><title>Success | Bitwarden CLI</title></head><body>" +
|
|
||||||
"<h1>Successfully authenticated with the Bitwarden CLI</h1>" +
|
|
||||||
"<p>You may now close this tab and return to the terminal.</p>" +
|
|
||||||
"</body></html>",
|
|
||||||
);
|
|
||||||
callbackServer.close(() =>
|
|
||||||
resolve({
|
|
||||||
ssoCode: code,
|
|
||||||
orgIdentifier: orgIdentifier,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
res.writeHead(400);
|
|
||||||
res.end(
|
|
||||||
"<html><head><title>Failed | Bitwarden CLI</title></head><body>" +
|
|
||||||
"<h1>Something went wrong logging into the Bitwarden CLI</h1>" +
|
|
||||||
"<p>You may now close this tab and return to the terminal.</p>" +
|
|
||||||
"</body></html>",
|
|
||||||
);
|
|
||||||
callbackServer.close(() => reject());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let foundPort = false;
|
|
||||||
const webUrl = this.environmentService.getWebVaultUrl();
|
|
||||||
for (let port = 8065; port <= 8070; port++) {
|
|
||||||
try {
|
|
||||||
this.ssoRedirectUri = "http://localhost:" + port;
|
|
||||||
callbackServer.listen(port, () => {
|
|
||||||
this.platformUtilsService.launchUri(
|
|
||||||
webUrl +
|
|
||||||
"/#/sso?clientId=" +
|
|
||||||
this.clientId +
|
|
||||||
"&redirectUri=" +
|
|
||||||
encodeURIComponent(this.ssoRedirectUri) +
|
|
||||||
"&state=" +
|
|
||||||
state +
|
|
||||||
"&codeChallenge=" +
|
|
||||||
codeChallenge,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
foundPort = true;
|
|
||||||
break;
|
|
||||||
} catch {
|
|
||||||
// Ignore error since we run the same command up to 5 times.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!foundPort) {
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private getOrgIdentifierFromState(state: string): string {
|
|
||||||
if (state === null || state === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateSplit = state.split("_identifier=");
|
|
||||||
return stateSplit.length > 1 ? stateSplit[1] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private checkState(state: string, checkState: string): boolean {
|
|
||||||
if (state === null || state === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (checkState === null || checkState === undefined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateSplit = state.split("_identifier=");
|
|
||||||
const checkStateSplit = checkState.split("_identifier=");
|
|
||||||
return stateSplit[0] === checkStateSplit[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,8 @@
|
|||||||
import * as child_process from "child_process";
|
|
||||||
|
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { ClientType } from "@/jslib/common/src/enums/clientType";
|
import { ClientType } from "@/jslib/common/src/enums/clientType";
|
||||||
import { DeviceType } from "@/jslib/common/src/enums/deviceType";
|
import { DeviceType } from "@/jslib/common/src/enums/deviceType";
|
||||||
import { ThemeType } from "@/jslib/common/src/enums/themeType";
|
import { ThemeType } from "@/jslib/common/src/enums/themeType";
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const open = require("open");
|
|
||||||
|
|
||||||
export class CliPlatformUtilsService implements PlatformUtilsService {
|
export class CliPlatformUtilsService implements PlatformUtilsService {
|
||||||
clientType: ClientType;
|
clientType: ClientType;
|
||||||
|
|
||||||
@@ -80,12 +75,8 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
|
|||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
launchUri(uri: string, options?: any): void {
|
launchUri(_uri: string, _options?: any): void {
|
||||||
if (process.platform === "linux") {
|
throw new Error("Not implemented.");
|
||||||
child_process.spawnSync("xdg-open", [uri]);
|
|
||||||
} else {
|
|
||||||
open(uri);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {
|
||||||
|
|||||||
2006
package-lock.json
generated
2006
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@@ -70,66 +70,67 @@
|
|||||||
"test:types": "npx tsc --noEmit"
|
"test:types": "npx tsc --noEmit"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@angular-devkit/build-angular": "16.2.12",
|
||||||
"@angular-eslint/eslint-plugin-template": "17.2.0",
|
"@angular-eslint/eslint-plugin-template": "17.2.0",
|
||||||
"@angular-eslint/template-parser": "17.2.0",
|
"@angular-eslint/template-parser": "17.2.0",
|
||||||
"@angular/compiler-cli": "16.2.12",
|
"@angular/compiler-cli": "16.2.12",
|
||||||
|
"@electron/notarize": "2.2.1",
|
||||||
|
"@electron/rebuild": "3.6.0",
|
||||||
"@fluffy-spoon/substitute": "1.208.0",
|
"@fluffy-spoon/substitute": "1.208.0",
|
||||||
"@microsoft/microsoft-graph-types": "2.40.0",
|
"@microsoft/microsoft-graph-types": "2.40.0",
|
||||||
"@ngtools/webpack": "16.2.12",
|
"@ngtools/webpack": "16.2.12",
|
||||||
"@types/inquirer": "8.2.6",
|
"@types/inquirer": "8.2.10",
|
||||||
"@types/jest": "29.5.11",
|
"@types/jest": "29.5.11",
|
||||||
"@types/ldapjs": "2.2.5",
|
|
||||||
"@types/lowdb": "1.0.15",
|
"@types/lowdb": "1.0.15",
|
||||||
"@types/node": "18.17.12",
|
"@types/node": "18.19.50",
|
||||||
"@types/node-fetch": "2.6.10",
|
"@types/node-fetch": "2.6.11",
|
||||||
"@types/node-forge": "1.3.11",
|
"@types/node-forge": "1.3.11",
|
||||||
"@types/proper-lockfile": "4.1.4",
|
"@types/proper-lockfile": "4.1.4",
|
||||||
"@types/tldjs": "2.3.4",
|
"@types/tldjs": "2.3.4",
|
||||||
"@types/zxcvbn": "4.4.4",
|
|
||||||
"@typescript-eslint/eslint-plugin": "5.62.0",
|
"@typescript-eslint/eslint-plugin": "5.62.0",
|
||||||
"@typescript-eslint/parser": "5.62.0",
|
"@typescript-eslint/parser": "5.62.0",
|
||||||
"clean-webpack-plugin": "4.0.0",
|
"clean-webpack-plugin": "4.0.0",
|
||||||
"concurrently": "8.2.2",
|
"concurrently": "9.0.1",
|
||||||
"copy-webpack-plugin": "12.0.2",
|
"copy-webpack-plugin": "12.0.2",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"css-loader": "6.9.0",
|
"css-loader": "6.11.0",
|
||||||
"dotenv": "16.4.1",
|
"dotenv": "16.4.5",
|
||||||
"electron": "28.2.0",
|
"electron": "28.3.3",
|
||||||
"electron-builder": "24.9.1",
|
"electron-builder": "24.9.1",
|
||||||
"electron-log": "5.0.1",
|
"electron-log": "5.2.0",
|
||||||
"@electron/notarize": "2.2.1",
|
|
||||||
"@electron/rebuild": "3.6.0",
|
|
||||||
"electron-reload": "2.0.0-alpha.1",
|
"electron-reload": "2.0.0-alpha.1",
|
||||||
"electron-store": "8.1.0",
|
"electron-store": "8.2.0",
|
||||||
"electron-updater": "6.1.7",
|
"electron-updater": "6.3.0",
|
||||||
"eslint": "8.56.0",
|
"eslint": "8.56.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-import-resolver-typescript": "3.6.1",
|
"eslint-import-resolver-typescript": "3.6.3",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.30.0",
|
||||||
"eslint-plugin-rxjs": "5.0.3",
|
"eslint-plugin-rxjs": "5.0.3",
|
||||||
"eslint-plugin-rxjs-angular": "2.0.1",
|
"eslint-plugin-rxjs-angular": "2.0.1",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"html-loader": "5.0.0",
|
"html-loader": "5.1.0",
|
||||||
"html-webpack-plugin": "5.6.0",
|
"html-webpack-plugin": "5.6.0",
|
||||||
"husky": "9.0.10",
|
"husky": "9.0.10",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"jest-junit": "16.0.0",
|
"jest-junit": "16.0.0",
|
||||||
"jest-preset-angular": "13.1.1",
|
"jest-mock-extended": "3.0.7",
|
||||||
|
"jest-preset-angular": "13.1.6",
|
||||||
"lint-staged": "15.2.10",
|
"lint-staged": "15.2.10",
|
||||||
"mini-css-extract-plugin": "2.7.7",
|
"mini-css-extract-plugin": "2.9.1",
|
||||||
"node-forge": "1.3.1",
|
"node-forge": "1.3.1",
|
||||||
"node-loader": "2.0.0",
|
"node-loader": "2.0.0",
|
||||||
"pkg": "5.8.1",
|
"pkg": "5.8.1",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
"rimraf": "5.0.10",
|
"rimraf": "5.0.10",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"sass": "1.69.7",
|
"sass": "1.78.0",
|
||||||
"sass-loader": "14.0.0",
|
"sass-loader": "16.0.1",
|
||||||
"ts-jest": "29.1.1",
|
"ts-jest": "29.2.5",
|
||||||
"ts-loader": "9.5.1",
|
"ts-loader": "9.5.1",
|
||||||
"tsconfig-paths-webpack-plugin": "4.1.0",
|
"tsconfig-paths-webpack-plugin": "4.1.0",
|
||||||
|
"type-fest": "3.13.1",
|
||||||
"typescript": "4.9.5",
|
"typescript": "4.9.5",
|
||||||
"typescript-transform-paths": "3.4.6",
|
"typescript-transform-paths": "3.5.1",
|
||||||
"webpack": "5.94.0",
|
"webpack": "5.94.0",
|
||||||
"webpack-cli": "5.1.4",
|
"webpack-cli": "5.1.4",
|
||||||
"webpack-merge": "6.0.1",
|
"webpack-merge": "6.0.1",
|
||||||
@@ -138,7 +139,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "16.2.12",
|
"@angular/animations": "16.2.12",
|
||||||
"@angular/cdk": "16.2.12",
|
"@angular/cdk": "16.2.14",
|
||||||
"@angular/common": "16.2.12",
|
"@angular/common": "16.2.12",
|
||||||
"@angular/compiler": "16.2.12",
|
"@angular/compiler": "16.2.12",
|
||||||
"@angular/core": "16.2.12",
|
"@angular/core": "16.2.12",
|
||||||
@@ -148,10 +149,10 @@
|
|||||||
"@angular/router": "16.2.12",
|
"@angular/router": "16.2.12",
|
||||||
"@microsoft/microsoft-graph-client": "3.0.7",
|
"@microsoft/microsoft-graph-client": "3.0.7",
|
||||||
"big-integer": "1.6.52",
|
"big-integer": "1.6.52",
|
||||||
"bootstrap": "4.6.2",
|
"bootstrap": "5.0.0",
|
||||||
"browser-hrtime": "1.1.8",
|
"browser-hrtime": "1.1.8",
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"commander": "12.0.0",
|
"commander": "12.1.0",
|
||||||
"core-js": "3.35.0",
|
"core-js": "3.35.0",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"google-auth-library": "7.14.1",
|
"google-auth-library": "7.14.1",
|
||||||
@@ -159,16 +160,14 @@
|
|||||||
"https-proxy-agent": "7.0.4",
|
"https-proxy-agent": "7.0.4",
|
||||||
"inquirer": "8.2.6",
|
"inquirer": "8.2.6",
|
||||||
"keytar": "7.9.0",
|
"keytar": "7.9.0",
|
||||||
"ldapjs": "2.3.3",
|
"ldapts": "7.2.0",
|
||||||
"lowdb": "1.0.0",
|
"lowdb": "1.0.0",
|
||||||
"ngx-toastr": "16.2.0",
|
"ngx-toastr": "16.2.0",
|
||||||
"node-fetch": "2.7.0",
|
"node-fetch": "2.7.0",
|
||||||
"open": "8.4.2",
|
|
||||||
"proper-lockfile": "4.1.2",
|
"proper-lockfile": "4.1.2",
|
||||||
"rxjs": "7.8.1",
|
"rxjs": "7.8.1",
|
||||||
"tldjs": "2.3.1",
|
"tldjs": "2.3.1",
|
||||||
"zone.js": "0.13.1",
|
"zone.js": "0.13.1"
|
||||||
"zxcvbn": "4.4.2"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "~18",
|
"node": "~18",
|
||||||
|
|||||||
4
src/abstractions/auth.service.ts
Normal file
4
src/abstractions/auth.service.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export abstract class AuthService {
|
||||||
|
logIn: (credentials: { clientId: string; clientSecret: string }) => Promise<void>;
|
||||||
|
logOut: (callback: () => void) => void;
|
||||||
|
}
|
||||||
@@ -2,41 +2,33 @@
|
|||||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-md-8 col-lg-6">
|
<div class="col-md-8 col-lg-6">
|
||||||
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
|
<p class="text-center fw-bold">{{ "welcome" | i18n }}</p>
|
||||||
<p class="text-center">{{ "logInDesc" | i18n }}</p>
|
<p class="text-center">{{ "logInDesc" | i18n }}</p>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
|
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="client_id">{{ "clientId" | i18n }}</label>
|
<label for="client_id" class="form-label">{{ "clientId" | i18n }}</label>
|
||||||
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
|
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="row-main">
|
<label for="client_secret" class="form-label">{{ "clientSecret" | i18n }}</label>
|
||||||
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
|
<div class="input-group">
|
||||||
<div class="input-group">
|
<input
|
||||||
<input
|
type="{{ showSecret ? 'text' : 'password' }}"
|
||||||
type="{{ showSecret ? 'text' : 'password' }}"
|
id="client_secret"
|
||||||
id="client_secret"
|
name="ClientSecret"
|
||||||
name="ClientSecret"
|
[(ngModel)]="clientSecret"
|
||||||
[(ngModel)]="clientSecret"
|
class="form-control"
|
||||||
class="form-control"
|
/>
|
||||||
/>
|
<button
|
||||||
<div class="input-group-append">
|
type="button"
|
||||||
<button
|
class="btn btn-outline-secondary"
|
||||||
type="button"
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
class="ml-1 btn btn-link"
|
(click)="toggleSecret()"
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
>
|
||||||
(click)="toggleSecret()"
|
<i class="bwi" [ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"></i>
|
||||||
>
|
</button>
|
||||||
<i
|
|
||||||
class="bwi bwi-lg"
|
|
||||||
aria-hidden="true"
|
|
||||||
[ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
<div class="d-flex">
|
||||||
@@ -47,7 +39,7 @@
|
|||||||
{{ "logIn" | i18n }}
|
{{ "logIn" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
|
<button type="button" class="btn btn-link ms-auto" (click)="settings()">
|
||||||
{{ "settings" | i18n }}
|
{{ "settings" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,13 +3,12 @@ import { Router } from "@angular/router";
|
|||||||
import { takeUntil } from "rxjs";
|
import { takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ModalService } from "@/jslib/angular/src/services/modal.service";
|
import { ModalService } from "@/jslib/angular/src/services/modal.service";
|
||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||||
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
|
|
||||||
|
import { AuthService } from "../../abstractions/auth.service";
|
||||||
import { StateService } from "../../abstractions/state.service";
|
import { StateService } from "../../abstractions/state.service";
|
||||||
|
|
||||||
import { EnvironmentComponent } from "./environment.component";
|
import { EnvironmentComponent } from "./environment.component";
|
||||||
@@ -81,9 +80,10 @@ export class ApiKeyComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.authService.logIn(
|
this.formPromise = this.authService.logIn({
|
||||||
new ApiLogInCredentials(this.clientId, this.clientSecret),
|
clientId: this.clientId,
|
||||||
);
|
clientSecret: this.clientSecret,
|
||||||
|
});
|
||||||
await this.formPromise;
|
await this.formPromise;
|
||||||
const organizationId = await this.stateService.getEntityId();
|
const organizationId = await this.stateService.getEntityId();
|
||||||
await this.stateService.setOrganizationId(organizationId);
|
await this.stateService.setOrganizationId(organizationId);
|
||||||
|
|||||||
@@ -3,15 +3,13 @@
|
|||||||
<form class="modal-content" (ngSubmit)="submit()">
|
<form class="modal-content" (ngSubmit)="submit()">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
|
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
|
||||||
<button type="button" class="close" data-dismiss="modal" title="Close">
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
|
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
|
||||||
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
|
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
|
<label for="baseUrl" class="form-label">{{ "baseUrl" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="baseUrl"
|
id="baseUrl"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -19,14 +17,12 @@
|
|||||||
[(ngModel)]="baseUrl"
|
[(ngModel)]="baseUrl"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text"
|
<div class="form-text">{{ "ex" | i18n }} https://bitwarden.company.com</div>
|
||||||
>{{ "ex" | i18n }} https://bitwarden.company.com</small
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<h4>{{ "customEnvironment" | i18n }}</h4>
|
<h4>{{ "customEnvironment" | i18n }}</h4>
|
||||||
<p>{{ "customEnvironmentFooter" | i18n }}</p>
|
<p>{{ "customEnvironmentFooter" | i18n }}</p>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
|
<label for="webVaultUrl" class="form-label">{{ "webVaultUrl" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="webVaultUrl"
|
id="webVaultUrl"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -35,12 +31,12 @@
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
|
<label for="apiUrl" class="form-label">{{ "apiUrl" | i18n }}</label>
|
||||||
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
|
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
|
<label for="identityUrl" class="form-label">{{ "identityUrl" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="identityUrl"
|
id="identityUrl"
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import { DomSanitizer } from "@angular/platform-browser";
|
|||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
||||||
|
|
||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { BroadcasterService } from "@/jslib/common/src/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@/jslib/common/src/abstractions/broadcaster.service";
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
@@ -18,6 +17,7 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
|
|||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
||||||
|
|
||||||
|
import { AuthService } from "../abstractions/auth.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { SyncService } from "../services/sync.service";
|
import { SyncService } from "../services/sync.service";
|
||||||
|
|
||||||
|
|||||||
17
src/app/services/injection-tokens.ts
Normal file
17
src/app/services/injection-tokens.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { InjectionToken } from "@angular/core";
|
||||||
|
|
||||||
|
import { StorageService } from "../../../jslib/common/src/abstractions/storage.service";
|
||||||
|
|
||||||
|
declare const tag: unique symbol;
|
||||||
|
/**
|
||||||
|
* A (more) typesafe version of InjectionToken which will more strictly enforce the generic type parameter.
|
||||||
|
* @remarks The default angular implementation does not use the generic type to define the structure of the object,
|
||||||
|
* so the structural type system will not complain about a mismatch in the type parameter.
|
||||||
|
* This is solved by assigning T to an arbitrary private property.
|
||||||
|
*/
|
||||||
|
export class SafeInjectionToken<T> extends InjectionToken<T> {
|
||||||
|
private readonly [tag]: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SECURE_STORAGE = new SafeInjectionToken<StorageService>("SECURE_STORAGE");
|
||||||
|
export const WINDOW = new SafeInjectionToken<Window>("WINDOW");
|
||||||
144
src/app/services/safe-provider.ts
Normal file
144
src/app/services/safe-provider.ts
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import { Provider } from "@angular/core";
|
||||||
|
import { Constructor, Opaque } from "type-fest";
|
||||||
|
|
||||||
|
import { SafeInjectionToken } from "./injection-tokens";
|
||||||
|
|
||||||
|
// ******
|
||||||
|
// NOTE: this is a copy/paste of safe-provider.ts from the clients repository.
|
||||||
|
// The clients repository remains the primary version of this code.
|
||||||
|
// Make any changes there and copy it back to this repository.
|
||||||
|
// ******
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The return type of the {@link safeProvider} helper function.
|
||||||
|
* Used to distinguish a type safe provider definition from a non-type safe provider definition.
|
||||||
|
*/
|
||||||
|
export type SafeProvider = Opaque<Provider>;
|
||||||
|
|
||||||
|
// TODO: type-fest also provides a type like this when we upgrade >= 3.7.0
|
||||||
|
type AbstractConstructor<T> = abstract new (...args: any) => T;
|
||||||
|
|
||||||
|
type MapParametersToDeps<T> = {
|
||||||
|
[K in keyof T]: AbstractConstructor<T[K]> | SafeInjectionToken<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SafeInjectionTokenType<T> = T extends SafeInjectionToken<infer J> ? J : never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the instance type from a constructor, abstract constructor, or SafeInjectionToken
|
||||||
|
*/
|
||||||
|
type ProviderInstanceType<T> =
|
||||||
|
T extends SafeInjectionToken<any>
|
||||||
|
? InstanceType<SafeInjectionTokenType<T>>
|
||||||
|
: T extends Constructor<any> | AbstractConstructor<any>
|
||||||
|
? InstanceType<T>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dependency provided with the useClass option.
|
||||||
|
*/
|
||||||
|
type SafeClassProvider<
|
||||||
|
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||||
|
I extends Constructor<ProviderInstanceType<A>>,
|
||||||
|
D extends MapParametersToDeps<ConstructorParameters<I>>,
|
||||||
|
> = {
|
||||||
|
provide: A;
|
||||||
|
useClass: I;
|
||||||
|
deps: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dependency provided with the useValue option.
|
||||||
|
*/
|
||||||
|
type SafeValueProvider<A extends SafeInjectionToken<any>, V extends SafeInjectionTokenType<A>> = {
|
||||||
|
provide: A;
|
||||||
|
useValue: V;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dependency provided with the useFactory option.
|
||||||
|
*/
|
||||||
|
type SafeFactoryProvider<
|
||||||
|
A extends AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||||
|
I extends (...args: any) => ProviderInstanceType<A>,
|
||||||
|
D extends MapParametersToDeps<Parameters<I>>,
|
||||||
|
> = {
|
||||||
|
provide: A;
|
||||||
|
useFactory: I;
|
||||||
|
deps: D;
|
||||||
|
multi?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dependency provided with the useExisting option.
|
||||||
|
*/
|
||||||
|
type SafeExistingProvider<
|
||||||
|
A extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||||
|
I extends Constructor<ProviderInstanceType<A>> | AbstractConstructor<ProviderInstanceType<A>>,
|
||||||
|
> = {
|
||||||
|
provide: A;
|
||||||
|
useExisting: I;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a dependency where there is no abstract token, the token is the implementation
|
||||||
|
*/
|
||||||
|
type SafeConcreteProvider<
|
||||||
|
I extends Constructor<any>,
|
||||||
|
D extends MapParametersToDeps<ConstructorParameters<I>>,
|
||||||
|
> = {
|
||||||
|
provide: I;
|
||||||
|
deps: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If useAngularDecorators: true is specified, do not require a deps array.
|
||||||
|
* This is a manual override for where @Injectable decorators are used
|
||||||
|
*/
|
||||||
|
type UseAngularDecorators<T extends { deps: any }> = Omit<T, "deps"> & {
|
||||||
|
useAngularDecorators: true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a type with a deps array that may optionally be overridden with useAngularDecorators
|
||||||
|
*/
|
||||||
|
type AllowAngularDecorators<T extends { deps: any }> = T | UseAngularDecorators<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory function that creates a provider for the ngModule providers array.
|
||||||
|
* This (almost) guarantees type safety for your provider definition. It does nothing at runtime.
|
||||||
|
* Warning: the useAngularDecorators option provides an override where your class uses the Injectable decorator,
|
||||||
|
* however this cannot be enforced by the type system and will not cause an error if the decorator is not used.
|
||||||
|
* @example safeProvider({ provide: MyService, useClass: DefaultMyService, deps: [AnotherService] })
|
||||||
|
* @param provider Your provider object in the usual shape (e.g. using useClass, useValue, useFactory, etc.)
|
||||||
|
* @returns The exact same object without modification (pass-through).
|
||||||
|
*/
|
||||||
|
export const safeProvider = <
|
||||||
|
// types for useClass
|
||||||
|
AClass extends AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||||
|
IClass extends Constructor<ProviderInstanceType<AClass>>,
|
||||||
|
DClass extends MapParametersToDeps<ConstructorParameters<IClass>>,
|
||||||
|
// types for useValue
|
||||||
|
AValue extends SafeInjectionToken<any>,
|
||||||
|
VValue extends SafeInjectionTokenType<AValue>,
|
||||||
|
// types for useFactory
|
||||||
|
AFactory extends AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||||
|
IFactory extends (...args: any) => ProviderInstanceType<AFactory>,
|
||||||
|
DFactory extends MapParametersToDeps<Parameters<IFactory>>,
|
||||||
|
// types for useExisting
|
||||||
|
AExisting extends Constructor<any> | AbstractConstructor<any> | SafeInjectionToken<any>,
|
||||||
|
IExisting extends
|
||||||
|
| Constructor<ProviderInstanceType<AExisting>>
|
||||||
|
| AbstractConstructor<ProviderInstanceType<AExisting>>,
|
||||||
|
// types for no token
|
||||||
|
IConcrete extends Constructor<any>,
|
||||||
|
DConcrete extends MapParametersToDeps<ConstructorParameters<IConcrete>>,
|
||||||
|
>(
|
||||||
|
provider:
|
||||||
|
| AllowAngularDecorators<SafeClassProvider<AClass, IClass, DClass>>
|
||||||
|
| SafeValueProvider<AValue, VValue>
|
||||||
|
| AllowAngularDecorators<SafeFactoryProvider<AFactory, IFactory, DFactory>>
|
||||||
|
| SafeExistingProvider<AExisting, IExisting>
|
||||||
|
| AllowAngularDecorators<SafeConcreteProvider<IConcrete, DConcrete>>
|
||||||
|
| Constructor<unknown>,
|
||||||
|
): SafeProvider => provider as SafeProvider;
|
||||||
@@ -3,20 +3,17 @@ import { APP_INITIALIZER, NgModule } from "@angular/core";
|
|||||||
import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module";
|
import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module";
|
||||||
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.service";
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
import { AppIdService as AppIdServiceAbstraction } from "@/jslib/common/src/abstractions/appId.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
|
import { BroadcasterService as BroadcasterServiceAbstraction } from "@/jslib/common/src/abstractions/broadcaster.service";
|
||||||
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
import { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
|
||||||
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
|
||||||
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
|
||||||
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService as I18nServiceAbstraction } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { LogService as LogServiceAbstraction } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService as LogServiceAbstraction } from "@/jslib/common/src/abstractions/log.service";
|
||||||
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
import { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
|
||||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
||||||
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
|
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
|
||||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||||
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
|
||||||
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||||
@@ -28,21 +25,23 @@ import { ElectronRendererStorageService } from "@/jslib/electron/src/services/el
|
|||||||
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
|
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.service";
|
||||||
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";
|
import { NodeCryptoFunctionService } from "@/jslib/node/src/services/nodeCryptoFunction.service";
|
||||||
|
|
||||||
|
import { AuthService as AuthServiceAbstraction } from "../../abstractions/auth.service";
|
||||||
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
|
import { StateService as StateServiceAbstraction } from "../../abstractions/state.service";
|
||||||
import { Account } from "../../models/account";
|
import { Account } from "../../models/account";
|
||||||
import { AuthService } from "../../services/auth.service";
|
import { AuthService } from "../../services/auth.service";
|
||||||
import { I18nService } from "../../services/i18n.service";
|
import { I18nService } from "../../services/i18n.service";
|
||||||
import { NoopTwoFactorService } from "../../services/noop/noopTwoFactor.service";
|
|
||||||
import { StateService } from "../../services/state.service";
|
import { StateService } from "../../services/state.service";
|
||||||
import { StateMigrationService } from "../../services/stateMigration.service";
|
import { StateMigrationService } from "../../services/stateMigration.service";
|
||||||
import { SyncService } from "../../services/sync.service";
|
import { SyncService } from "../../services/sync.service";
|
||||||
|
|
||||||
import { AuthGuardService } from "./auth-guard.service";
|
import { AuthGuardService } from "./auth-guard.service";
|
||||||
|
import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from "./injection-tokens";
|
||||||
import { LaunchGuardService } from "./launch-guard.service";
|
import { LaunchGuardService } from "./launch-guard.service";
|
||||||
|
import { SafeProvider, safeProvider } from "./safe-provider";
|
||||||
|
|
||||||
export function initFactory(
|
export function initFactory(
|
||||||
environmentService: EnvironmentServiceAbstraction,
|
environmentService: EnvironmentServiceAbstraction,
|
||||||
i18nService: I18nService,
|
i18nService: I18nServiceAbstraction,
|
||||||
platformUtilsService: PlatformUtilsServiceAbstraction,
|
platformUtilsService: PlatformUtilsServiceAbstraction,
|
||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
cryptoService: CryptoServiceAbstraction,
|
cryptoService: CryptoServiceAbstraction,
|
||||||
@@ -50,7 +49,7 @@ export function initFactory(
|
|||||||
return async () => {
|
return async () => {
|
||||||
await stateService.init();
|
await stateService.init();
|
||||||
await environmentService.setUrlsFromStorage();
|
await environmentService.setUrlsFromStorage();
|
||||||
await i18nService.init();
|
await (i18nService as I18nService).init();
|
||||||
const htmlEl = window.document.documentElement;
|
const htmlEl = window.document.documentElement;
|
||||||
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
|
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
|
||||||
htmlEl.classList.add("locale_" + i18nService.translationLocale);
|
htmlEl.classList.add("locale_" + i18nService.translationLocale);
|
||||||
@@ -78,8 +77,8 @@ export function initFactory(
|
|||||||
imports: [JslibServicesModule],
|
imports: [JslibServicesModule],
|
||||||
declarations: [],
|
declarations: [],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
safeProvider({
|
||||||
provide: APP_INITIALIZER,
|
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
|
||||||
useFactory: initFactory,
|
useFactory: initFactory,
|
||||||
deps: [
|
deps: [
|
||||||
EnvironmentServiceAbstraction,
|
EnvironmentServiceAbstraction,
|
||||||
@@ -89,21 +88,29 @@ export function initFactory(
|
|||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
],
|
],
|
||||||
multi: true,
|
multi: true,
|
||||||
},
|
}),
|
||||||
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
|
safeProvider({ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }),
|
||||||
{
|
safeProvider({
|
||||||
provide: I18nServiceAbstraction,
|
provide: I18nServiceAbstraction,
|
||||||
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
|
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
|
||||||
deps: ["WINDOW"],
|
deps: [WINDOW],
|
||||||
},
|
}),
|
||||||
{
|
safeProvider({
|
||||||
provide: MessagingServiceAbstraction,
|
provide: MessagingServiceAbstraction,
|
||||||
useClass: ElectronRendererMessagingService,
|
useClass: ElectronRendererMessagingService,
|
||||||
deps: [BroadcasterServiceAbstraction],
|
deps: [BroadcasterServiceAbstraction],
|
||||||
},
|
}),
|
||||||
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
|
safeProvider({
|
||||||
{ provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService },
|
provide: StorageServiceAbstraction,
|
||||||
{
|
useClass: ElectronRendererStorageService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
|
provide: SECURE_STORAGE,
|
||||||
|
useClass: ElectronRendererSecureStorageService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
provide: PlatformUtilsServiceAbstraction,
|
provide: PlatformUtilsServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
i18nService: I18nServiceAbstraction,
|
i18nService: I18nServiceAbstraction,
|
||||||
@@ -111,9 +118,13 @@ export function initFactory(
|
|||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
|
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
|
||||||
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
|
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
|
||||||
},
|
}),
|
||||||
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] },
|
safeProvider({
|
||||||
{
|
provide: CryptoFunctionServiceAbstraction,
|
||||||
|
useClass: NodeCryptoFunctionService,
|
||||||
|
deps: [],
|
||||||
|
}),
|
||||||
|
safeProvider({
|
||||||
provide: ApiServiceAbstraction,
|
provide: ApiServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
tokenService: TokenServiceAbstraction,
|
tokenService: TokenServiceAbstraction,
|
||||||
@@ -141,26 +152,19 @@ export function initFactory(
|
|||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
{
|
safeProvider({
|
||||||
provide: AuthServiceAbstraction,
|
provide: AuthServiceAbstraction,
|
||||||
useClass: AuthService,
|
useClass: AuthService,
|
||||||
deps: [
|
deps: [
|
||||||
CryptoServiceAbstraction,
|
|
||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
TokenServiceAbstraction,
|
|
||||||
AppIdServiceAbstraction,
|
AppIdServiceAbstraction,
|
||||||
PlatformUtilsServiceAbstraction,
|
PlatformUtilsServiceAbstraction,
|
||||||
MessagingServiceAbstraction,
|
MessagingServiceAbstraction,
|
||||||
LogServiceAbstraction,
|
|
||||||
KeyConnectorServiceAbstraction,
|
|
||||||
EnvironmentServiceAbstraction,
|
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
TwoFactorServiceAbstraction,
|
|
||||||
I18nServiceAbstraction,
|
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
{
|
safeProvider({
|
||||||
provide: SyncService,
|
provide: SyncService,
|
||||||
useClass: SyncService,
|
useClass: SyncService,
|
||||||
deps: [
|
deps: [
|
||||||
@@ -172,10 +176,10 @@ export function initFactory(
|
|||||||
EnvironmentServiceAbstraction,
|
EnvironmentServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
AuthGuardService,
|
safeProvider(AuthGuardService),
|
||||||
LaunchGuardService,
|
safeProvider(LaunchGuardService),
|
||||||
{
|
safeProvider({
|
||||||
provide: StateMigrationServiceAbstraction,
|
provide: StateMigrationServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -186,9 +190,9 @@ export function initFactory(
|
|||||||
secureStorageService,
|
secureStorageService,
|
||||||
new StateFactory(GlobalState, Account),
|
new StateFactory(GlobalState, Account),
|
||||||
),
|
),
|
||||||
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
|
deps: [StorageServiceAbstraction, SECURE_STORAGE],
|
||||||
},
|
}),
|
||||||
{
|
safeProvider({
|
||||||
provide: StateServiceAbstraction,
|
provide: StateServiceAbstraction,
|
||||||
useFactory: (
|
useFactory: (
|
||||||
storageService: StorageServiceAbstraction,
|
storageService: StorageServiceAbstraction,
|
||||||
@@ -206,15 +210,11 @@ export function initFactory(
|
|||||||
),
|
),
|
||||||
deps: [
|
deps: [
|
||||||
StorageServiceAbstraction,
|
StorageServiceAbstraction,
|
||||||
"SECURE_STORAGE",
|
SECURE_STORAGE,
|
||||||
LogServiceAbstraction,
|
LogServiceAbstraction,
|
||||||
StateMigrationServiceAbstraction,
|
StateMigrationServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
{
|
] satisfies SafeProvider[],
|
||||||
provide: TwoFactorServiceAbstraction,
|
|
||||||
useClass: NoopTwoFactorService,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
export class ServicesModule {}
|
export class ServicesModule {}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
<div class="row">
|
<div class="row g-3">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<h3 class="card-header">{{ "directory" | i18n }}</h3>
|
<h3 class="card-header">{{ "directory" | i18n }}</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="directory">{{ "type" | i18n }}</label>
|
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
|
||||||
<select class="form-control" id="directory" name="Directory" [(ngModel)]="directory">
|
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
|
||||||
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
|
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
|
||||||
{{ o.name }}
|
{{ o.name }}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap">
|
<div [hidden]="directory != directoryType.Ldap">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="hostname">{{ "serverHostname" | i18n }}</label>
|
<label for="hostname" class="form-label">{{ "serverHostname" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -21,15 +21,15 @@
|
|||||||
name="Hostname"
|
name="Hostname"
|
||||||
[(ngModel)]="ldap.hostname"
|
[(ngModel)]="ldap.hostname"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} ad.company.com</small>
|
<div class="form-text">{{ "ex" | i18n }} ad.company.com</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="port">{{ "port" | i18n }}</label>
|
<label for="port" class="form-label">{{ "port" | i18n }}</label>
|
||||||
<input type="text" class="form-control" id="port" name="Port" [(ngModel)]="ldap.port" />
|
<input type="text" class="form-control" id="port" name="Port" [(ngModel)]="ldap.port" />
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} 389</small>
|
<div class="form-text">{{ "ex" | i18n }} 389</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="rootPath">{{ "rootPath" | i18n }}</label>
|
<label for="rootPath" class="form-label">{{ "rootPath" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
name="RootPath"
|
name="RootPath"
|
||||||
[(ngModel)]="ldap.rootPath"
|
[(ngModel)]="ldap.rootPath"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} dc=company,dc=com</small>
|
<div class="form-text">{{ "ex" | i18n }} dc=company,dc=com</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
|
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" *ngIf="!ldap.ad">
|
<div class="mb-3" *ngIf="!ldap.ad">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
}}</label>
|
}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -79,38 +79,38 @@
|
|||||||
}}</label>
|
}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4" *ngIf="ldap.ssl">
|
<div class="ms-4" *ngIf="ldap.ssl">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-radio">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-radio-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
[value]="false"
|
[value]="false"
|
||||||
id="ssl"
|
id="ssl"
|
||||||
[(ngModel)]="ldap.startTls"
|
[(ngModel)]="ldap.startTls"
|
||||||
name="SSL"
|
name="SSL"
|
||||||
/>
|
/>
|
||||||
<label class="form-radio-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
|
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-radio">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-radio-input"
|
class="form-check-input"
|
||||||
type="radio"
|
type="radio"
|
||||||
[value]="true"
|
[value]="true"
|
||||||
id="startTls"
|
id="startTls"
|
||||||
[(ngModel)]="ldap.startTls"
|
[(ngModel)]="ldap.startTls"
|
||||||
name="StartTLS"
|
name="StartTLS"
|
||||||
/>
|
/>
|
||||||
<label class="form-radio-label" for="startTls">{{ "ldapTls" | i18n }}</label>
|
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4" *ngIf="ldap.startTls">
|
<div class="ms-4" *ngIf="ldap.startTls">
|
||||||
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
|
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="tlsCaPath">{{ "ldapTlsCa" | i18n }}</label>
|
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
class="form-control-file mb-2"
|
class="form-control mb-2"
|
||||||
id="tlsCaPath_file"
|
id="tlsCaPath_file"
|
||||||
(change)="setSslPath('tlsCaPath')"
|
(change)="setSslPath('tlsCaPath')"
|
||||||
/>
|
/>
|
||||||
@@ -123,13 +123,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4" *ngIf="!ldap.startTls">
|
<div class="ms-4" *ngIf="!ldap.startTls">
|
||||||
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
|
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="sslCertPath">{{ "ldapSslCert" | i18n }}</label>
|
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
class="form-control-file mb-2"
|
class="form-control mb-2"
|
||||||
id="sslCertPath_file"
|
id="sslCertPath_file"
|
||||||
(change)="setSslPath('sslCertPath')"
|
(change)="setSslPath('sslCertPath')"
|
||||||
/>
|
/>
|
||||||
@@ -141,11 +141,11 @@
|
|||||||
[(ngModel)]="ldap.sslCertPath"
|
[(ngModel)]="ldap.sslCertPath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="sslKeyPath">{{ "ldapSslKey" | i18n }}</label>
|
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
class="form-control-file mb-2"
|
class="form-control mb-2"
|
||||||
id="sslKeyPath_file"
|
id="sslKeyPath_file"
|
||||||
(change)="setSslPath('sslKeyPath')"
|
(change)="setSslPath('sslKeyPath')"
|
||||||
/>
|
/>
|
||||||
@@ -157,11 +157,11 @@
|
|||||||
[(ngModel)]="ldap.sslKeyPath"
|
[(ngModel)]="ldap.sslKeyPath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="sslCaPath">{{ "ldapSslCa" | i18n }}</label>
|
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
class="form-control-file mb-2"
|
class="form-control mb-2"
|
||||||
id="sslCaPath_file"
|
id="sslCaPath_file"
|
||||||
(change)="setSslPath('sslCaPath')"
|
(change)="setSslPath('sslCaPath')"
|
||||||
/>
|
/>
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -189,7 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" [hidden]="true">
|
<div class="mb-3" [hidden]="true">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -202,8 +202,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="ldap.currentUser">
|
<div [hidden]="ldap.currentUser">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="username">{{ "username" | i18n }}</label>
|
<label for="username" class="form-label">{{ "username" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -211,15 +211,13 @@
|
|||||||
name="Username"
|
name="Username"
|
||||||
[(ngModel)]="ldap.username"
|
[(ngModel)]="ldap.username"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text" *ngIf="ldap.ad"
|
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} company\admin</div>
|
||||||
>{{ "ex" | i18n }} company\admin</small
|
<div class="form-text" *ngIf="!ldap.ad">
|
||||||
>
|
{{ "ex" | i18n }} cn=admin,dc=company,dc=com
|
||||||
<small class="text-muted form-text" *ngIf="!ldap.ad"
|
</div>
|
||||||
>{{ "ex" | i18n }} cn=admin,dc=company,dc=com</small
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="password">{{ "password" | i18n }}</label>
|
<label for="password" class="form-label">{{ "password" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="{{ showLdapPassword ? 'text' : 'password' }}"
|
type="{{ showLdapPassword ? 'text' : 'password' }}"
|
||||||
@@ -228,29 +226,29 @@
|
|||||||
name="Password"
|
name="Password"
|
||||||
[(ngModel)]="ldap.password"
|
[(ngModel)]="ldap.password"
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="btn btn-outline-secondary"
|
||||||
class="btn btn-outline-secondary"
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
(click)="toggleLdapPassword()"
|
||||||
(click)="toggleLdapPassword()"
|
>
|
||||||
>
|
<i
|
||||||
<i
|
class="bwi bwi-lg"
|
||||||
class="bwi bwi-lg"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
[ngClass]="showLdapPassword ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||||
[ngClass]="showLdapPassword ? 'bwi-eye-slash' : 'bwi-eye'"
|
></i>
|
||||||
></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.AzureActiveDirectory">
|
<div [hidden]="directory != directoryType.AzureActiveDirectory">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="identityAuthority">{{ "identityAuthority" | i18n }}</label>
|
<label for="identityAuthority" class="form-label">{{
|
||||||
|
"identityAuthority" | i18n
|
||||||
|
}}</label>
|
||||||
<select
|
<select
|
||||||
class="form-control"
|
class="form-select"
|
||||||
id="identityAuthority"
|
id="identityAuthority"
|
||||||
name="IdentityAuthority"
|
name="IdentityAuthority"
|
||||||
[(ngModel)]="azure.identityAuthority"
|
[(ngModel)]="azure.identityAuthority"
|
||||||
@@ -259,8 +257,8 @@
|
|||||||
<option value="login.microsoftonline.us">Azure AD Government</option>
|
<option value="login.microsoftonline.us">Azure AD Government</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="tenant">{{ "tenant" | i18n }}</label>
|
<label for="tenant" class="form-label">{{ "tenant" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -268,10 +266,10 @@
|
|||||||
name="Tenant"
|
name="Tenant"
|
||||||
[(ngModel)]="azure.tenant"
|
[(ngModel)]="azure.tenant"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</small>
|
<div class="form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="applicationId">{{ "applicationId" | i18n }}</label>
|
<label for="applicationId" class="form-label">{{ "applicationId" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -280,8 +278,8 @@
|
|||||||
[(ngModel)]="azure.applicationId"
|
[(ngModel)]="azure.applicationId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="secretKey">{{ "secretKey" | i18n }}</label>
|
<label for="secretKey" class="form-label">{{ "secretKey" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="{{ showAzureKey ? 'text' : 'password' }}"
|
type="{{ showAzureKey ? 'text' : 'password' }}"
|
||||||
@@ -290,26 +288,24 @@
|
|||||||
name="SecretKey"
|
name="SecretKey"
|
||||||
[(ngModel)]="azure.key"
|
[(ngModel)]="azure.key"
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="btn btn-outline-secondary"
|
||||||
class="btn btn-outline-secondary"
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
(click)="toggleAzureKey()"
|
||||||
(click)="toggleAzureKey()"
|
>
|
||||||
>
|
<i
|
||||||
<i
|
class="bwi bwi-lg"
|
||||||
class="bwi bwi-lg"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||||
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'"
|
></i>
|
||||||
></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Okta">
|
<div [hidden]="directory != directoryType.Okta">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="orgUrl">{{ "organizationUrl" | i18n }}</label>
|
<label for="orgUrl" class="form-label">{{ "organizationUrl" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -317,12 +313,10 @@
|
|||||||
name="OrgUrl"
|
name="OrgUrl"
|
||||||
[(ngModel)]="okta.orgUrl"
|
[(ngModel)]="okta.orgUrl"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text"
|
<div class="form-text">{{ "ex" | i18n }} https://mycompany.okta.com/</div>
|
||||||
>{{ "ex" | i18n }} https://mycompany.okta.com/</small
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="oktaToken">{{ "token" | i18n }}</label>
|
<label for="oktaToken" class="form-label">{{ "token" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="{{ showOktaKey ? 'text' : 'password' }}"
|
type="{{ showOktaKey ? 'text' : 'password' }}"
|
||||||
@@ -331,26 +325,24 @@
|
|||||||
name="OktaToken"
|
name="OktaToken"
|
||||||
[(ngModel)]="okta.token"
|
[(ngModel)]="okta.token"
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="btn btn-outline-secondary"
|
||||||
class="btn btn-outline-secondary"
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
(click)="toggleOktaKey()"
|
||||||
(click)="toggleOktaKey()"
|
>
|
||||||
>
|
<i
|
||||||
<i
|
class="bwi bwi-lg"
|
||||||
class="bwi bwi-lg"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
[ngClass]="showOktaKey ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||||
[ngClass]="showOktaKey ? 'bwi-eye-slash' : 'bwi-eye'"
|
></i>
|
||||||
></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.OneLogin">
|
<div [hidden]="directory != directoryType.OneLogin">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="oneLoginClientId">{{ "clientId" | i18n }}</label>
|
<label for="oneLoginClientId" class="form-label">{{ "clientId" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -359,8 +351,8 @@
|
|||||||
[(ngModel)]="oneLogin.clientId"
|
[(ngModel)]="oneLogin.clientId"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="oneLoginClientSecret">{{ "clientSecret" | i18n }}</label>
|
<label for="oneLoginClientSecret" class="form-label">{{ "clientSecret" | i18n }}</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input
|
||||||
type="{{ showOneLoginSecret ? 'text' : 'password' }}"
|
type="{{ showOneLoginSecret ? 'text' : 'password' }}"
|
||||||
@@ -369,26 +361,24 @@
|
|||||||
name="OneLoginClientSecret"
|
name="OneLoginClientSecret"
|
||||||
[(ngModel)]="oneLogin.clientSecret"
|
[(ngModel)]="oneLogin.clientSecret"
|
||||||
/>
|
/>
|
||||||
<div class="input-group-append">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
class="btn btn-outline-secondary"
|
||||||
class="btn btn-outline-secondary"
|
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
||||||
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
|
(click)="toggleOneLoginSecret()"
|
||||||
(click)="toggleOneLoginSecret()"
|
>
|
||||||
>
|
<i
|
||||||
<i
|
class="bwi bwi-lg"
|
||||||
class="bwi bwi-lg"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
[ngClass]="showOneLoginSecret ? 'bwi-eye-slash' : 'bwi-eye'"
|
||||||
[ngClass]="showOneLoginSecret ? 'bwi-eye-slash' : 'bwi-eye'"
|
></i>
|
||||||
></i>
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="oneLoginRegion">{{ "region" | i18n }}</label>
|
<label for="oneLoginRegion" class="form-label">{{ "region" | i18n }}</label>
|
||||||
<select
|
<select
|
||||||
class="form-control"
|
class="form-select"
|
||||||
id="oneLoginRegion"
|
id="oneLoginRegion"
|
||||||
name="OneLoginRegion"
|
name="OneLoginRegion"
|
||||||
[(ngModel)]="oneLogin.region"
|
[(ngModel)]="oneLogin.region"
|
||||||
@@ -399,8 +389,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.GSuite">
|
<div [hidden]="directory != directoryType.GSuite">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="domain">{{ "domain" | i18n }}</label>
|
<label for="domain" class="form-label">{{ "domain" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -408,10 +398,10 @@
|
|||||||
name="Domain"
|
name="Domain"
|
||||||
[(ngModel)]="gsuite.domain"
|
[(ngModel)]="gsuite.domain"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} company.com</small>
|
<div class="form-text">{{ "ex" | i18n }} company.com</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="adminUser">{{ "adminUser" | i18n }}</label>
|
<label for="adminUser" class="form-label">{{ "adminUser" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -419,10 +409,10 @@
|
|||||||
name="AdminUser"
|
name="AdminUser"
|
||||||
[(ngModel)]="gsuite.adminUser"
|
[(ngModel)]="gsuite.adminUser"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} admin@company.com</small>
|
<div class="form-text">{{ "ex" | i18n }} admin@company.com</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="customerId">{{ "customerId" | i18n }}</label>
|
<label for="customerId" class="form-label">{{ "customerId" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -430,21 +420,21 @@
|
|||||||
name="CustomerId"
|
name="CustomerId"
|
||||||
[(ngModel)]="gsuite.customer"
|
[(ngModel)]="gsuite.customer"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} 39204722352</small>
|
<div class="form-text">{{ "ex" | i18n }} 39204722352</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="keyFile">{{ "jsonKeyFile" | i18n }}</label>
|
<label for="keyFile" class="form-label">{{ "jsonKeyFile" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
class="form-control-file"
|
class="form-control"
|
||||||
id="keyFile"
|
id="keyFile"
|
||||||
accept=".json"
|
accept=".json"
|
||||||
(change)="parseKeyFile()"
|
(change)="parseKeyFile()"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} My Project-jksd3jd223.json</small>
|
<div class="form-text">{{ "ex" | i18n }} My Project-jksd3jd223.json</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" [hidden]="!gsuite.clientEmail">
|
<div class="mb-3" [hidden]="!gsuite.clientEmail">
|
||||||
<label for="clientEmail">{{ "clientEmail" | i18n }}</label>
|
<label for="clientEmail" class="form-label">{{ "clientEmail" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -453,8 +443,8 @@
|
|||||||
[(ngModel)]="gsuite.clientEmail"
|
[(ngModel)]="gsuite.clientEmail"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" [hidden]="!gsuite.privateKey">
|
<div class="mb-3" [hidden]="!gsuite.privateKey">
|
||||||
<label for="privateKey">{{ "privateKey" | i18n }}</label>
|
<label for="privateKey" class="form-label">{{ "privateKey" | i18n }}</label>
|
||||||
<textarea
|
<textarea
|
||||||
class="form-control text-monospace"
|
class="form-control text-monospace"
|
||||||
id="privateKey"
|
id="privateKey"
|
||||||
@@ -471,8 +461,8 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-header">{{ "sync" | i18n }}</h3>
|
<h3 class="card-header">{{ "sync" | i18n }}</h3>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="interval">{{ "interval" | i18n }}</label>
|
<label for="interval" class="form-label">{{ "interval" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min="5"
|
min="5"
|
||||||
@@ -481,9 +471,9 @@
|
|||||||
name="Interval"
|
name="Interval"
|
||||||
[(ngModel)]="sync.interval"
|
[(ngModel)]="sync.interval"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "intervalMin" | i18n }}</small>
|
<div class="form-text">{{ "intervalMin" | i18n }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -497,7 +487,7 @@
|
|||||||
}}</label>
|
}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -511,7 +501,7 @@
|
|||||||
}}</label>
|
}}</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -525,8 +515,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap">
|
<div [hidden]="directory != directoryType.Ldap">
|
||||||
<div [hidden]="ldap.ad">
|
<div [hidden]="ldap.ad">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="memberAttribute">{{ "memberAttribute" | i18n }}</label>
|
<label for="memberAttribute" class="form-label">{{ "memberAttribute" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -534,10 +524,12 @@
|
|||||||
name="MemberAttribute"
|
name="MemberAttribute"
|
||||||
[(ngModel)]="sync.memberAttribute"
|
[(ngModel)]="sync.memberAttribute"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} uniqueMember</small>
|
<div class="form-text">{{ "ex" | i18n }} uniqueMember</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="creationDateAttribute">{{ "creationDateAttribute" | i18n }}</label>
|
<label for="creationDateAttribute" class="form-label">{{
|
||||||
|
"creationDateAttribute" | i18n
|
||||||
|
}}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -545,10 +537,12 @@
|
|||||||
name="CreationDateAttribute"
|
name="CreationDateAttribute"
|
||||||
[(ngModel)]="sync.creationDateAttribute"
|
[(ngModel)]="sync.creationDateAttribute"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} whenCreated</small>
|
<div class="form-text">{{ "ex" | i18n }} whenCreated</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="revisionDateAttribute">{{ "revisionDateAttribute" | i18n }}</label>
|
<label for="revisionDateAttribute" class="form-label">{{
|
||||||
|
"revisionDateAttribute" | i18n
|
||||||
|
}}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -556,12 +550,12 @@
|
|||||||
name="RevisionDateAttribute"
|
name="RevisionDateAttribute"
|
||||||
[(ngModel)]="sync.revisionDateAttribute"
|
[(ngModel)]="sync.revisionDateAttribute"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} whenChanged</small>
|
<div class="form-text">{{ "ex" | i18n }} whenChanged</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
|
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -576,8 +570,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="!sync.useEmailPrefixSuffix">
|
<div [hidden]="!sync.useEmailPrefixSuffix">
|
||||||
<div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap">
|
<div class="mb-3" [hidden]="ldap.ad || directory != directoryType.Ldap">
|
||||||
<label for="emailPrefixAttribute">{{ "emailPrefixAttribute" | i18n }}</label>
|
<label for="emailPrefixAttribute" class="form-label">{{
|
||||||
|
"emailPrefixAttribute" | i18n
|
||||||
|
}}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -585,10 +581,10 @@
|
|||||||
name="EmailPrefixAttribute"
|
name="EmailPrefixAttribute"
|
||||||
[(ngModel)]="sync.emailPrefixAttribute"
|
[(ngModel)]="sync.emailPrefixAttribute"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} accountName</small>
|
<div class="form-text">{{ "ex" | i18n }} accountName</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="emailSuffix">{{ "emailSuffix" | i18n }}</label>
|
<label for="emailSuffix" class="form-label">{{ "emailSuffix" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -596,12 +592,12 @@
|
|||||||
name="EmailSuffix"
|
name="EmailSuffix"
|
||||||
[(ngModel)]="sync.emailSuffix"
|
[(ngModel)]="sync.emailSuffix"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} @company.com</small>
|
<div class="form-text">{{ "ex" | i18n }} @company.com</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -614,31 +610,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="!sync.users">
|
<div [hidden]="!sync.users">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="userFilter">{{ "userFilter" | i18n }}</label>
|
<label for="userFilter" class="form-label">{{ "userFilter" | i18n }}</label>
|
||||||
<textarea
|
<textarea
|
||||||
class="form-control"
|
class="form-control"
|
||||||
id="userFilter"
|
id="userFilter"
|
||||||
name="UserFilter"
|
name="UserFilter"
|
||||||
[(ngModel)]="sync.userFilter"
|
[(ngModel)]="sync.userFilter"
|
||||||
></textarea>
|
></textarea>
|
||||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap"
|
<div class="form-text" *ngIf="directory === directoryType.Ldap">
|
||||||
>{{ "ex" | i18n }} (&(givenName=John)(|(l=Dallas)(l=Austin)))</small
|
{{ "ex" | i18n }} (&(givenName=John)(|(l=Dallas)(l=Austin)))
|
||||||
>
|
</div>
|
||||||
<small
|
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory">
|
||||||
class="text-muted form-text"
|
{{ "ex" | i18n }} exclude:joe@company.com
|
||||||
*ngIf="directory === directoryType.AzureActiveDirectory"
|
</div>
|
||||||
>{{ "ex" | i18n }} exclude:joe@company.com</small
|
<div class="form-text" *ngIf="directory === directoryType.Okta">
|
||||||
>
|
{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"
|
||||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
|
</div>
|
||||||
>{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"</small
|
<div class="form-text" *ngIf="directory === directoryType.GSuite">
|
||||||
>
|
{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering
|
||||||
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
|
</div>
|
||||||
>{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering</small
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" [hidden]="directory != directoryType.Ldap">
|
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
||||||
<label for="userPath">{{ "userPath" | i18n }}</label>
|
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -646,11 +640,11 @@
|
|||||||
name="UserPath"
|
name="UserPath"
|
||||||
[(ngModel)]="sync.userPath"
|
[(ngModel)]="sync.userPath"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} CN=Users</small>
|
<div class="form-text">{{ "ex" | i18n }} CN=Users</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="userObjectClass">{{ "userObjectClass" | i18n }}</label>
|
<label for="userObjectClass" class="form-label">{{ "userObjectClass" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -658,10 +652,12 @@
|
|||||||
name="UserObjectClass"
|
name="UserObjectClass"
|
||||||
[(ngModel)]="sync.userObjectClass"
|
[(ngModel)]="sync.userObjectClass"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} inetOrgPerson</small>
|
<div class="form-text">{{ "ex" | i18n }} inetOrgPerson</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="userEmailAttribute">{{ "userEmailAttribute" | i18n }}</label>
|
<label for="userEmailAttribute" class="form-label">{{
|
||||||
|
"userEmailAttribute" | i18n
|
||||||
|
}}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -669,12 +665,12 @@
|
|||||||
name="UserEmailAttribute"
|
name="UserEmailAttribute"
|
||||||
[(ngModel)]="sync.userEmailAttribute"
|
[(ngModel)]="sync.userEmailAttribute"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} mail</small>
|
<div class="form-text">{{ "ex" | i18n }} mail</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
@@ -689,8 +685,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="!sync.groups">
|
<div [hidden]="!sync.groups">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="groupFilter">{{
|
<label for="groupFilter" class="form-label">{{
|
||||||
(directory !== directoryType.OneLogin ? "groupFilter" : "groupFilterOneLogin") | i18n
|
(directory !== directoryType.OneLogin ? "groupFilter" : "groupFilterOneLogin") | i18n
|
||||||
}}</label>
|
}}</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -699,23 +695,21 @@
|
|||||||
name="GroupFilter"
|
name="GroupFilter"
|
||||||
[(ngModel)]="sync.groupFilter"
|
[(ngModel)]="sync.groupFilter"
|
||||||
></textarea>
|
></textarea>
|
||||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap"
|
<div class="form-text" *ngIf="directory === directoryType.Ldap">
|
||||||
>{{ "ex" | i18n }} (&(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))</small
|
{{ "ex" | i18n }} (&(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
|
||||||
>
|
</div>
|
||||||
<small
|
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory">
|
||||||
class="text-muted form-text"
|
{{ "ex" | i18n }} include:Sales,IT
|
||||||
*ngIf="directory === directoryType.AzureActiveDirectory"
|
</div>
|
||||||
>{{ "ex" | i18n }} include:Sales,IT</small
|
<div class="form-text" *ngIf="directory === directoryType.Okta">
|
||||||
>
|
{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"
|
||||||
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
|
</div>
|
||||||
>{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</small
|
<div class="form-text" *ngIf="directory === directoryType.GSuite">
|
||||||
>
|
{{ "ex" | i18n }} include:Sales,IT
|
||||||
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
|
</div>
|
||||||
>{{ "ex" | i18n }} include:Sales,IT</small
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group" [hidden]="directory != directoryType.Ldap">
|
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
|
||||||
<label for="groupPath">{{ "groupPath" | i18n }}</label>
|
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -723,12 +717,14 @@
|
|||||||
name="GroupPath"
|
name="GroupPath"
|
||||||
[(ngModel)]="sync.groupPath"
|
[(ngModel)]="sync.groupPath"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</small>
|
<div class="form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</div>
|
||||||
<small class="text-muted form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</small>
|
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</div>
|
||||||
</div>
|
</div>
|
||||||
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="groupObjectClass">{{ "groupObjectClass" | i18n }}</label>
|
<label for="groupObjectClass" class="form-label">{{
|
||||||
|
"groupObjectClass" | i18n
|
||||||
|
}}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -736,10 +732,12 @@
|
|||||||
name="GroupObjectClass"
|
name="GroupObjectClass"
|
||||||
[(ngModel)]="sync.groupObjectClass"
|
[(ngModel)]="sync.groupObjectClass"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} groupOfUniqueNames</small>
|
<div class="form-text">{{ "ex" | i18n }} groupOfUniqueNames</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="mb-3">
|
||||||
<label for="groupNameAttribute">{{ "groupNameAttribute" | i18n }}</label>
|
<label for="groupNameAttribute" class="form-label">{{
|
||||||
|
"groupNameAttribute" | i18n
|
||||||
|
}}</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -747,7 +745,7 @@
|
|||||||
name="GroupNameAttribute"
|
name="GroupNameAttribute"
|
||||||
[(ngModel)]="sync.groupNameAttribute"
|
[(ngModel)]="sync.groupNameAttribute"
|
||||||
/>
|
/>
|
||||||
<small class="text-muted form-text">{{ "ex" | i18n }} name</small>
|
<div class="form-text">{{ "ex" | i18n }} name</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
47
src/bwdc.ts
47
src/bwdc.ts
@@ -2,7 +2,6 @@ import * as fs from "fs";
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
|
||||||
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { ClientType } from "@/jslib/common/src/enums/clientType";
|
import { ClientType } from "@/jslib/common/src/enums/clientType";
|
||||||
import { LogLevelType } from "@/jslib/common/src/enums/logLevelType";
|
import { LogLevelType } from "@/jslib/common/src/enums/logLevelType";
|
||||||
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
|
||||||
@@ -11,11 +10,7 @@ import { AppIdService } from "@/jslib/common/src/services/appId.service";
|
|||||||
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
import { ContainerService } from "@/jslib/common/src/services/container.service";
|
||||||
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
|
||||||
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
|
|
||||||
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
|
import { NoopMessagingService } from "@/jslib/common/src/services/noopMessaging.service";
|
||||||
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
|
|
||||||
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
|
|
||||||
import { PolicyService } from "@/jslib/common/src/services/policy.service";
|
|
||||||
import { TokenService } from "@/jslib/common/src/services/token.service";
|
import { TokenService } from "@/jslib/common/src/services/token.service";
|
||||||
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
|
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
|
||||||
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
|
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
|
||||||
@@ -28,7 +23,6 @@ import { AuthService } from "./services/auth.service";
|
|||||||
import { I18nService } from "./services/i18n.service";
|
import { I18nService } from "./services/i18n.service";
|
||||||
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
|
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
|
||||||
import { LowdbStorageService } from "./services/lowdbStorage.service";
|
import { LowdbStorageService } from "./services/lowdbStorage.service";
|
||||||
import { NoopTwoFactorService } from "./services/noop/noopTwoFactor.service";
|
|
||||||
import { StateService } from "./services/state.service";
|
import { StateService } from "./services/state.service";
|
||||||
import { StateMigrationService } from "./services/stateMigration.service";
|
import { StateMigrationService } from "./services/stateMigration.service";
|
||||||
import { SyncService } from "./services/sync.service";
|
import { SyncService } from "./services/sync.service";
|
||||||
@@ -39,6 +33,8 @@ const packageJson = require("../package.json");
|
|||||||
export class Main {
|
export class Main {
|
||||||
dataFilePath: string;
|
dataFilePath: string;
|
||||||
logService: ConsoleLogService;
|
logService: ConsoleLogService;
|
||||||
|
program: Program;
|
||||||
|
|
||||||
messagingService: NoopMessagingService;
|
messagingService: NoopMessagingService;
|
||||||
storageService: LowdbStorageService;
|
storageService: LowdbStorageService;
|
||||||
secureStorageService: StorageServiceAbstraction;
|
secureStorageService: StorageServiceAbstraction;
|
||||||
@@ -53,14 +49,8 @@ export class Main {
|
|||||||
cryptoFunctionService: NodeCryptoFunctionService;
|
cryptoFunctionService: NodeCryptoFunctionService;
|
||||||
authService: AuthService;
|
authService: AuthService;
|
||||||
syncService: SyncService;
|
syncService: SyncService;
|
||||||
passwordGenerationService: PasswordGenerationService;
|
|
||||||
policyService: PolicyService;
|
|
||||||
keyConnectorService: KeyConnectorService;
|
|
||||||
program: Program;
|
|
||||||
stateService: StateService;
|
stateService: StateService;
|
||||||
stateMigrationService: StateMigrationService;
|
stateMigrationService: StateMigrationService;
|
||||||
organizationService: OrganizationService;
|
|
||||||
twoFactorService: TwoFactorServiceAbstraction;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const applicationName = "Bitwarden Directory Connector";
|
const applicationName = "Bitwarden Directory Connector";
|
||||||
@@ -148,33 +138,12 @@ export class Main {
|
|||||||
);
|
);
|
||||||
this.containerService = new ContainerService(this.cryptoService);
|
this.containerService = new ContainerService(this.cryptoService);
|
||||||
|
|
||||||
this.organizationService = new OrganizationService(this.stateService);
|
|
||||||
|
|
||||||
this.keyConnectorService = new KeyConnectorService(
|
|
||||||
this.stateService,
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.logService,
|
|
||||||
this.organizationService,
|
|
||||||
this.cryptoFunctionService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.twoFactorService = new NoopTwoFactorService();
|
|
||||||
|
|
||||||
this.authService = new AuthService(
|
this.authService = new AuthService(
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
this.appIdService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
this.logService,
|
|
||||||
this.keyConnectorService,
|
|
||||||
this.environmentService,
|
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.twoFactorService,
|
|
||||||
this.i18nService,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.syncService = new SyncService(
|
this.syncService = new SyncService(
|
||||||
@@ -187,18 +156,6 @@ export class Main {
|
|||||||
this.stateService,
|
this.stateService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.policyService = new PolicyService(
|
|
||||||
this.stateService,
|
|
||||||
this.organizationService,
|
|
||||||
this.apiService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.passwordGenerationService = new PasswordGenerationService(
|
|
||||||
this.cryptoService,
|
|
||||||
this.policyService,
|
|
||||||
this.stateService,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.program = new Program(this);
|
this.program = new Program(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
60
src/commands/login.command.spec.ts
Normal file
60
src/commands/login.command.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { AuthService } from "../abstractions/auth.service";
|
||||||
|
|
||||||
|
import { LoginCommand } from "./login.command";
|
||||||
|
|
||||||
|
const clientId = "test_client_id";
|
||||||
|
const clientSecret = "test_client_secret";
|
||||||
|
|
||||||
|
// Mock responses from the inquirer prompt
|
||||||
|
// This combines both prompt results into a single object which is returned both times
|
||||||
|
jest.mock("inquirer", () => ({
|
||||||
|
createPromptModule: () => () => ({
|
||||||
|
clientId,
|
||||||
|
clientSecret,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("LoginCommand", () => {
|
||||||
|
let authService: MockProxy<AuthService>;
|
||||||
|
|
||||||
|
let loginCommand: LoginCommand;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// reset env variables
|
||||||
|
delete process.env.BW_CLIENTID;
|
||||||
|
delete process.env.BW_CLIENTSECRET;
|
||||||
|
|
||||||
|
authService = mock();
|
||||||
|
|
||||||
|
loginCommand = new LoginCommand(authService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses client id and secret stored in environment variables", async () => {
|
||||||
|
process.env.BW_CLIENTID = clientId;
|
||||||
|
process.env.BW_CLIENTSECRET = clientSecret;
|
||||||
|
|
||||||
|
const result = await loginCommand.run();
|
||||||
|
|
||||||
|
expect(authService.logIn).toHaveBeenCalledWith({ clientId, clientSecret });
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
title: "You are logged in!",
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses client id and secret prompted from the user", async () => {
|
||||||
|
const result = await loginCommand.run();
|
||||||
|
|
||||||
|
expect(authService.logIn).toHaveBeenCalledWith({ clientId, clientSecret });
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
data: {
|
||||||
|
title: "You are logged in!",
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
87
src/commands/login.command.ts
Normal file
87
src/commands/login.command.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import * as inquirer from "inquirer";
|
||||||
|
|
||||||
|
import { Response } from "@/jslib/node/src/cli/models/response";
|
||||||
|
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
|
||||||
|
|
||||||
|
import { Utils } from "../../jslib/common/src/misc/utils";
|
||||||
|
import { AuthService } from "../abstractions/auth.service";
|
||||||
|
|
||||||
|
export class LoginCommand {
|
||||||
|
private canInteract: boolean;
|
||||||
|
|
||||||
|
constructor(private authService: AuthService) {}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
this.canInteract = process.env.BW_NOINTERACTION !== "true";
|
||||||
|
|
||||||
|
const { clientId, clientSecret } = await this.apiIdentifiers();
|
||||||
|
|
||||||
|
if (Utils.isNullOrWhitespace(clientId)) {
|
||||||
|
return Response.error("Client ID is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Utils.isNullOrWhitespace(clientSecret)) {
|
||||||
|
return Response.error("Client Secret is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.authService.logIn({ clientId, clientSecret });
|
||||||
|
|
||||||
|
const res = new MessageResponse("You are logged in!", null);
|
||||||
|
return Response.success(res);
|
||||||
|
} catch (e) {
|
||||||
|
return Response.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiClientId(): Promise<string> {
|
||||||
|
let clientId: string = null;
|
||||||
|
|
||||||
|
const storedClientId: string = process.env.BW_CLIENTID;
|
||||||
|
if (storedClientId == null) {
|
||||||
|
if (this.canInteract) {
|
||||||
|
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||||
|
output: process.stderr,
|
||||||
|
})({
|
||||||
|
type: "input",
|
||||||
|
name: "clientId",
|
||||||
|
message: "client_id:",
|
||||||
|
});
|
||||||
|
clientId = answer.clientId;
|
||||||
|
} else {
|
||||||
|
clientId = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clientId = storedClientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiClientSecret(): Promise<string> {
|
||||||
|
let clientSecret: string = null;
|
||||||
|
|
||||||
|
const storedClientSecret = process.env.BW_CLIENTSECRET;
|
||||||
|
if (this.canInteract && storedClientSecret == null) {
|
||||||
|
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||||
|
output: process.stderr,
|
||||||
|
})({
|
||||||
|
type: "input",
|
||||||
|
name: "clientSecret",
|
||||||
|
message: "client_secret:",
|
||||||
|
});
|
||||||
|
clientSecret = answer.clientSecret;
|
||||||
|
} else {
|
||||||
|
clientSecret = storedClientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async apiIdentifiers(): Promise<{ clientId: string; clientSecret: string }> {
|
||||||
|
return {
|
||||||
|
clientId: await this.apiClientId(),
|
||||||
|
clientSecret: await this.apiClientSecret(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
|
||||||
|
|
||||||
import { Response } from "../models/response";
|
import { Response } from "@/jslib/node/src/cli/models/response";
|
||||||
import { MessageResponse } from "../models/response/messageResponse";
|
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
|
||||||
|
|
||||||
|
import { AuthService } from "../abstractions/auth.service";
|
||||||
|
|
||||||
export class LogoutCommand {
|
export class LogoutCommand {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private i18nService: I18nService,
|
|
||||||
private logoutCallback: () => Promise<void>,
|
private logoutCallback: () => Promise<void>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import { MenuMain } from "./menu.main";
|
|||||||
const SyncCheckInterval = 60 * 1000; // 1 minute
|
const SyncCheckInterval = 60 * 1000; // 1 minute
|
||||||
|
|
||||||
export class MessagingMain {
|
export class MessagingMain {
|
||||||
private syncTimeout: NodeJS.Timer;
|
private syncTimeout: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private windowMain: WindowMain,
|
private windowMain: WindowMain,
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import { LogInStrategy } from "@/jslib/common/src/misc/logInStrategies/logIn.strategy";
|
|
||||||
import {
|
|
||||||
AccountKeys,
|
|
||||||
AccountProfile,
|
|
||||||
AccountTokens,
|
|
||||||
} from "@/jslib/common/src/models/domain/account";
|
|
||||||
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
|
|
||||||
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
|
||||||
import { ApiTokenRequest } from "@/jslib/common/src/models/request/identityToken/apiTokenRequest";
|
|
||||||
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
|
||||||
|
|
||||||
import { Account, DirectoryConfigurations, DirectorySettings } from "@/src/models/account";
|
|
||||||
|
|
||||||
export class OrganizationLogInStrategy extends LogInStrategy {
|
|
||||||
tokenRequest: ApiTokenRequest;
|
|
||||||
|
|
||||||
async logIn(credentials: ApiLogInCredentials) {
|
|
||||||
this.tokenRequest = new ApiTokenRequest(
|
|
||||||
credentials.clientId,
|
|
||||||
credentials.clientSecret,
|
|
||||||
await this.buildTwoFactor(),
|
|
||||||
await this.buildDeviceRequest(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.startLogIn();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async processTokenResponse(response: IdentityTokenResponse): Promise<AuthResult> {
|
|
||||||
await this.saveAccountInformation(response);
|
|
||||||
return new AuthResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
|
||||||
const clientId = this.tokenRequest.clientId;
|
|
||||||
const entityId = clientId.split("organization.")[1];
|
|
||||||
const clientSecret = this.tokenRequest.clientSecret;
|
|
||||||
|
|
||||||
await this.stateService.addAccount(
|
|
||||||
new Account({
|
|
||||||
profile: {
|
|
||||||
...new AccountProfile(),
|
|
||||||
...{
|
|
||||||
userId: entityId,
|
|
||||||
apiKeyClientId: clientId,
|
|
||||||
entityId: entityId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tokens: {
|
|
||||||
...new AccountTokens(),
|
|
||||||
...{
|
|
||||||
accessToken: tokenResponse.accessToken,
|
|
||||||
refreshToken: tokenResponse.refreshToken,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
keys: {
|
|
||||||
...new AccountKeys(),
|
|
||||||
...{
|
|
||||||
apiKeyClientSecret: clientSecret,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
directorySettings: new DirectorySettings(),
|
|
||||||
directoryConfigurations: new DirectoryConfigurations(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,8 +5,6 @@ import { Command, OptionValues } from "commander";
|
|||||||
|
|
||||||
import { Utils } from "@/jslib/common/src/misc/utils";
|
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||||
import { BaseProgram } from "@/jslib/node/src/cli/baseProgram";
|
import { BaseProgram } from "@/jslib/node/src/cli/baseProgram";
|
||||||
import { LoginCommand } from "@/jslib/node/src/cli/commands/login.command";
|
|
||||||
import { LogoutCommand } from "@/jslib/node/src/cli/commands/logout.command";
|
|
||||||
import { UpdateCommand } from "@/jslib/node/src/cli/commands/update.command";
|
import { UpdateCommand } from "@/jslib/node/src/cli/commands/update.command";
|
||||||
import { Response } from "@/jslib/node/src/cli/models/response";
|
import { Response } from "@/jslib/node/src/cli/models/response";
|
||||||
import { StringResponse } from "@/jslib/node/src/cli/models/response/stringResponse";
|
import { StringResponse } from "@/jslib/node/src/cli/models/response/stringResponse";
|
||||||
@@ -15,6 +13,8 @@ import { Main } from "./bwdc";
|
|||||||
import { ClearCacheCommand } from "./commands/clearCache.command";
|
import { ClearCacheCommand } from "./commands/clearCache.command";
|
||||||
import { ConfigCommand } from "./commands/config.command";
|
import { ConfigCommand } from "./commands/config.command";
|
||||||
import { LastSyncCommand } from "./commands/lastSync.command";
|
import { LastSyncCommand } from "./commands/lastSync.command";
|
||||||
|
import { LoginCommand } from "./commands/login.command";
|
||||||
|
import { LogoutCommand } from "./commands/logout.command";
|
||||||
import { SyncCommand } from "./commands/sync.command";
|
import { SyncCommand } from "./commands/sync.command";
|
||||||
import { TestCommand } from "./commands/test.command";
|
import { TestCommand } from "./commands/test.command";
|
||||||
|
|
||||||
@@ -92,20 +92,7 @@ export class Program extends BaseProgram {
|
|||||||
})
|
})
|
||||||
.action(async (clientId: string, clientSecret: string, options: OptionValues) => {
|
.action(async (clientId: string, clientSecret: string, options: OptionValues) => {
|
||||||
await this.exitIfAuthed();
|
await this.exitIfAuthed();
|
||||||
const command = new LoginCommand(
|
const command = new LoginCommand(this.main.authService);
|
||||||
this.main.authService,
|
|
||||||
this.main.apiService,
|
|
||||||
this.main.i18nService,
|
|
||||||
this.main.environmentService,
|
|
||||||
this.main.passwordGenerationService,
|
|
||||||
this.main.cryptoFunctionService,
|
|
||||||
this.main.platformUtilsService,
|
|
||||||
this.main.stateService,
|
|
||||||
this.main.cryptoService,
|
|
||||||
this.main.policyService,
|
|
||||||
this.main.twoFactorService,
|
|
||||||
"connector",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!Utils.isNullOrWhitespace(clientId)) {
|
if (!Utils.isNullOrWhitespace(clientId)) {
|
||||||
process.env.BW_CLIENTID = clientId;
|
process.env.BW_CLIENTID = clientId;
|
||||||
@@ -114,8 +101,7 @@ export class Program extends BaseProgram {
|
|||||||
process.env.BW_CLIENTSECRET = clientSecret;
|
process.env.BW_CLIENTSECRET = clientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
|
const response = await command.run();
|
||||||
const response = await command.run(null, null, options);
|
|
||||||
this.processResponse(response);
|
this.processResponse(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,7 +118,6 @@ export class Program extends BaseProgram {
|
|||||||
await this.exitIfNotAuthed();
|
await this.exitIfNotAuthed();
|
||||||
const command = new LogoutCommand(
|
const command = new LogoutCommand(
|
||||||
this.main.authService,
|
this.main.authService,
|
||||||
this.main.i18nService,
|
|
||||||
async () => await this.main.logout(),
|
async () => await this.main.logout(),
|
||||||
);
|
);
|
||||||
const response = await command.run();
|
const response = await command.run();
|
||||||
|
|||||||
@@ -138,6 +138,6 @@ ul.testing-list {
|
|||||||
|
|
||||||
&:focus,
|
&:focus,
|
||||||
&.focus {
|
&.focus {
|
||||||
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.5);
|
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-contrast($primary), $primary, 15%), 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,91 @@
|
|||||||
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||||
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||||
import { CryptoService } from "@/jslib/common/src/abstractions/crypto.service";
|
|
||||||
import { EnvironmentService } from "@/jslib/common/src/abstractions/environment.service";
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
|
||||||
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
|
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
|
||||||
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
|
||||||
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
|
import {
|
||||||
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
|
AccountKeys,
|
||||||
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
|
AccountProfile,
|
||||||
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
|
AccountTokens,
|
||||||
import { AuthService as AuthServiceBase } from "@/jslib/common/src/services/auth.service";
|
} from "@/jslib/common/src/models/domain/account";
|
||||||
|
import { DeviceRequest } from "@/jslib/common/src/models/request/deviceRequest";
|
||||||
|
import { ApiTokenRequest } from "@/jslib/common/src/models/request/identityToken/apiTokenRequest";
|
||||||
|
import { TokenRequestTwoFactor } from "@/jslib/common/src/models/request/identityToken/tokenRequestTwoFactor";
|
||||||
|
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
||||||
|
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { OrganizationLogInStrategy } from "../misc/logInStrategies/organizationLogIn.strategy";
|
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
|
||||||
|
|
||||||
export class AuthService extends AuthServiceBase {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
cryptoService: CryptoService,
|
private apiService: ApiService,
|
||||||
apiService: ApiService,
|
private appIdService: AppIdService,
|
||||||
tokenService: TokenService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
appIdService: AppIdService,
|
private messagingService: MessagingService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
private stateService: StateService,
|
||||||
messagingService: MessagingService,
|
) {}
|
||||||
logService: LogService,
|
|
||||||
keyConnectorService: KeyConnectorService,
|
async logIn(credentials: { clientId: string; clientSecret: string }) {
|
||||||
environmentService: EnvironmentService,
|
const tokenRequest = new ApiTokenRequest(
|
||||||
stateService: StateService,
|
credentials.clientId,
|
||||||
twoFactorService: TwoFactorService,
|
credentials.clientSecret,
|
||||||
i18nService: I18nService,
|
new TokenRequestTwoFactor(), // unused
|
||||||
) {
|
await this.buildDeviceRequest(),
|
||||||
super(
|
|
||||||
cryptoService,
|
|
||||||
apiService,
|
|
||||||
tokenService,
|
|
||||||
appIdService,
|
|
||||||
platformUtilsService,
|
|
||||||
messagingService,
|
|
||||||
logService,
|
|
||||||
keyConnectorService,
|
|
||||||
environmentService,
|
|
||||||
stateService,
|
|
||||||
twoFactorService,
|
|
||||||
i18nService,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const response = await this.apiService.postIdentityToken(tokenRequest);
|
||||||
|
|
||||||
|
if (response instanceof IdentityTokenResponse) {
|
||||||
|
await this.saveAccountInformation(tokenRequest, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Invalid response object.");
|
||||||
}
|
}
|
||||||
|
|
||||||
async logIn(credentials: ApiLogInCredentials): Promise<AuthResult> {
|
logOut(callback: () => void) {
|
||||||
const strategy = new OrganizationLogInStrategy(
|
callback();
|
||||||
this.cryptoService,
|
this.messagingService.send("loggedOut");
|
||||||
this.apiService,
|
}
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
);
|
|
||||||
|
|
||||||
return strategy.logIn(credentials);
|
private async buildDeviceRequest() {
|
||||||
|
const appId = await this.appIdService.getAppId();
|
||||||
|
return new DeviceRequest(appId, this.platformUtilsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveAccountInformation(
|
||||||
|
tokenRequest: ApiTokenRequest,
|
||||||
|
tokenResponse: IdentityTokenResponse,
|
||||||
|
) {
|
||||||
|
const clientId = tokenRequest.clientId;
|
||||||
|
const entityId = clientId.split("organization.")[1];
|
||||||
|
const clientSecret = tokenRequest.clientSecret;
|
||||||
|
|
||||||
|
await this.stateService.addAccount(
|
||||||
|
new Account({
|
||||||
|
profile: {
|
||||||
|
...new AccountProfile(),
|
||||||
|
...{
|
||||||
|
userId: entityId,
|
||||||
|
apiKeyClientId: clientId,
|
||||||
|
entityId: entityId,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
...new AccountTokens(),
|
||||||
|
...{
|
||||||
|
accessToken: tokenResponse.accessToken,
|
||||||
|
refreshToken: tokenResponse.refreshToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
keys: {
|
||||||
|
...new AccountKeys(),
|
||||||
|
...{
|
||||||
|
apiKeyClientSecret: clientSecret,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directorySettings: new DirectorySettings(),
|
||||||
|
directoryConfigurations: new DirectoryConfigurations(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
97
src/services/authService.spec.ts
Normal file
97
src/services/authService.spec.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { Arg, Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
||||||
|
|
||||||
|
import { ApiService } from "@/jslib/common/src/abstractions/api.service";
|
||||||
|
import { AppIdService } from "@/jslib/common/src/abstractions/appId.service";
|
||||||
|
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
|
||||||
|
import { Utils } from "@/jslib/common/src/misc/utils";
|
||||||
|
import {
|
||||||
|
AccountKeys,
|
||||||
|
AccountProfile,
|
||||||
|
AccountTokens,
|
||||||
|
} from "@/jslib/common/src/models/domain/account";
|
||||||
|
import { IdentityTokenResponse } from "@/jslib/common/src/models/response/identityTokenResponse";
|
||||||
|
|
||||||
|
import { MessagingService } from "../../jslib/common/src/abstractions/messaging.service";
|
||||||
|
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
|
||||||
|
|
||||||
|
import { AuthService } from "./auth.service";
|
||||||
|
import { StateService } from "./state.service";
|
||||||
|
|
||||||
|
const clientId = "organization.CLIENT_ID";
|
||||||
|
const clientSecret = "CLIENT_SECRET";
|
||||||
|
|
||||||
|
const deviceId = Utils.newGuid();
|
||||||
|
const accessToken = "ACCESS_TOKEN";
|
||||||
|
const refreshToken = "REFRESH_TOKEN";
|
||||||
|
|
||||||
|
export function identityTokenResponseFactory() {
|
||||||
|
return new IdentityTokenResponse({
|
||||||
|
access_token: accessToken,
|
||||||
|
refresh_token: refreshToken, // not actually sure this is sent but including it out of caution
|
||||||
|
expires_in: 3600,
|
||||||
|
token_type: "Bearer",
|
||||||
|
scope: "api.organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("AuthService", () => {
|
||||||
|
let apiService: SubstituteOf<ApiService>;
|
||||||
|
let appIdService: SubstituteOf<AppIdService>;
|
||||||
|
let platformUtilsService: SubstituteOf<PlatformUtilsService>;
|
||||||
|
let messagingService: SubstituteOf<MessagingService>;
|
||||||
|
let stateService: SubstituteOf<StateService>;
|
||||||
|
|
||||||
|
let authService: AuthService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
apiService = Substitute.for();
|
||||||
|
appIdService = Substitute.for();
|
||||||
|
platformUtilsService = Substitute.for();
|
||||||
|
stateService = Substitute.for();
|
||||||
|
messagingService = Substitute.for();
|
||||||
|
|
||||||
|
appIdService.getAppId().resolves(deviceId);
|
||||||
|
|
||||||
|
authService = new AuthService(
|
||||||
|
apiService,
|
||||||
|
appIdService,
|
||||||
|
platformUtilsService,
|
||||||
|
messagingService,
|
||||||
|
stateService,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the local environment after a successful login", async () => {
|
||||||
|
apiService.postIdentityToken(Arg.any()).resolves(identityTokenResponseFactory());
|
||||||
|
|
||||||
|
await authService.logIn({ clientId, clientSecret });
|
||||||
|
|
||||||
|
stateService.received(1).addAccount(
|
||||||
|
new Account({
|
||||||
|
profile: {
|
||||||
|
...new AccountProfile(),
|
||||||
|
...{
|
||||||
|
userId: "CLIENT_ID",
|
||||||
|
apiKeyClientId: clientId, // with the "organization." prefix
|
||||||
|
entityId: "CLIENT_ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tokens: {
|
||||||
|
...new AccountTokens(),
|
||||||
|
...{
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
keys: {
|
||||||
|
...new AccountKeys(),
|
||||||
|
...{
|
||||||
|
apiKeyClientSecret: clientSecret,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
directorySettings: new DirectorySettings(),
|
||||||
|
directoryConfigurations: new DirectoryConfigurations(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from "fs";
|
import * as fs from "fs";
|
||||||
import { checkServerIdentity, PeerCertificate } from "tls";
|
import * as tls from "tls";
|
||||||
|
|
||||||
import * as ldap from "ldapjs";
|
import * as ldapts from "ldapts";
|
||||||
|
|
||||||
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
|
||||||
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
import { LogService } from "@/jslib/common/src/abstractions/log.service";
|
||||||
@@ -19,7 +19,7 @@ import { IDirectoryService } from "./directory.service";
|
|||||||
const UserControlAccountDisabled = 2;
|
const UserControlAccountDisabled = 2;
|
||||||
|
|
||||||
export class LdapDirectoryService implements IDirectoryService {
|
export class LdapDirectoryService implements IDirectoryService {
|
||||||
private client: ldap.Client;
|
private client: ldapts.Client;
|
||||||
private dirConfig: LdapConfiguration;
|
private dirConfig: LdapConfiguration;
|
||||||
private syncConfig: SyncConfiguration;
|
private syncConfig: SyncConfiguration;
|
||||||
|
|
||||||
@@ -48,21 +48,25 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
await this.bind();
|
await this.bind();
|
||||||
|
|
||||||
let users: UserEntry[];
|
let users: UserEntry[];
|
||||||
if (this.syncConfig.users) {
|
|
||||||
users = await this.getUsers(force, test);
|
|
||||||
}
|
|
||||||
|
|
||||||
let groups: GroupEntry[];
|
let groups: GroupEntry[];
|
||||||
if (this.syncConfig.groups) {
|
|
||||||
let groupForce = force;
|
try {
|
||||||
if (!groupForce && users != null) {
|
if (this.syncConfig.users) {
|
||||||
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
|
users = await this.getUsers(force, test);
|
||||||
groupForce = activeUsers.length > 0;
|
|
||||||
}
|
}
|
||||||
groups = await this.getGroups(groupForce);
|
|
||||||
|
if (this.syncConfig.groups) {
|
||||||
|
let groupForce = force;
|
||||||
|
if (!groupForce && users != null) {
|
||||||
|
const activeUsers = users.filter((u) => !u.deleted && !u.disabled);
|
||||||
|
groupForce = activeUsers.length > 0;
|
||||||
|
}
|
||||||
|
groups = await this.getGroups(groupForce);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await this.client.unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.unbind();
|
|
||||||
return [groups, users];
|
return [groups, users];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,10 +105,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
|
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
|
||||||
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
|
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
|
||||||
|
|
||||||
const delControl = new (ldap as any).Control({
|
const delControl = new ldapts.Control("1.2.840.113556.1.4.417", { critical: true });
|
||||||
type: "1.2.840.113556.1.4.417",
|
|
||||||
criticality: true,
|
|
||||||
});
|
|
||||||
const deletedUsers = await this.search<UserEntry>(
|
const deletedUsers = await this.search<UserEntry>(
|
||||||
deletedPath,
|
deletedPath,
|
||||||
deletedFilter,
|
deletedFilter,
|
||||||
@@ -334,144 +335,93 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
path: string,
|
path: string,
|
||||||
filter: string,
|
filter: string,
|
||||||
processEntry: (searchEntry: any) => T,
|
processEntry: (searchEntry: any) => T,
|
||||||
controls: ldap.Control[] = [],
|
controls: ldapts.Control[] = [],
|
||||||
): Promise<T[]> {
|
): Promise<T[]> {
|
||||||
const options: ldap.SearchOptions = {
|
const options: ldapts.SearchOptions = {
|
||||||
filter: filter,
|
filter: filter,
|
||||||
scope: "sub",
|
scope: "sub",
|
||||||
paged: this.dirConfig.pagedSearch,
|
paged: this.dirConfig.pagedSearch,
|
||||||
};
|
};
|
||||||
const entries: T[] = [];
|
const { searchEntries } = await this.client.search(path, options, controls);
|
||||||
return new Promise<T[]>((resolve, reject) => {
|
return searchEntries.map((e) => processEntry(e)).filter((e) => e != null);
|
||||||
this.client.search(path, options, controls, (err, res) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.on("error", (resErr) => {
|
|
||||||
reject(resErr);
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("searchEntry", (entry) => {
|
|
||||||
const e = processEntry(entry);
|
|
||||||
if (e != null) {
|
|
||||||
entries.push(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.on("end", (result) => {
|
|
||||||
resolve(entries);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async bind(): Promise<any> {
|
private async bind(): Promise<any> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
||||||
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
|
throw new Error(this.i18nService.t("dirConfigIncomplete"));
|
||||||
reject(this.i18nService.t("dirConfigIncomplete"));
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : "");
|
|
||||||
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port;
|
|
||||||
const options: ldap.ClientOptions = {
|
|
||||||
url: url.trim().toLowerCase(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const tlsOptions: any = {};
|
const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : "");
|
||||||
if (this.dirConfig.ssl) {
|
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port;
|
||||||
if (this.dirConfig.sslAllowUnauthorized) {
|
const options: ldapts.ClientOptions = {
|
||||||
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
|
url: url.trim().toLowerCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const tlsOptions: tls.ConnectionOptions = {};
|
||||||
|
if (this.dirConfig.ssl) {
|
||||||
|
if (this.dirConfig.sslAllowUnauthorized) {
|
||||||
|
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
|
||||||
|
}
|
||||||
|
if (!this.dirConfig.startTls) {
|
||||||
|
if (
|
||||||
|
this.dirConfig.sslCaPath != null &&
|
||||||
|
this.dirConfig.sslCaPath !== "" &&
|
||||||
|
fs.existsSync(this.dirConfig.sslCaPath)
|
||||||
|
) {
|
||||||
|
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
|
||||||
}
|
}
|
||||||
if (!this.dirConfig.startTls) {
|
if (
|
||||||
if (
|
this.dirConfig.sslCertPath != null &&
|
||||||
this.dirConfig.sslCaPath != null &&
|
this.dirConfig.sslCertPath !== "" &&
|
||||||
this.dirConfig.sslCaPath !== "" &&
|
fs.existsSync(this.dirConfig.sslCertPath)
|
||||||
fs.existsSync(this.dirConfig.sslCaPath)
|
) {
|
||||||
) {
|
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
|
||||||
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
|
}
|
||||||
}
|
if (
|
||||||
if (
|
this.dirConfig.sslKeyPath != null &&
|
||||||
this.dirConfig.sslCertPath != null &&
|
this.dirConfig.sslKeyPath !== "" &&
|
||||||
this.dirConfig.sslCertPath !== "" &&
|
fs.existsSync(this.dirConfig.sslKeyPath)
|
||||||
fs.existsSync(this.dirConfig.sslCertPath)
|
) {
|
||||||
) {
|
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
|
||||||
tlsOptions.cert = fs.readFileSync(this.dirConfig.sslCertPath);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
this.dirConfig.sslKeyPath != null &&
|
|
||||||
this.dirConfig.sslKeyPath !== "" &&
|
|
||||||
fs.existsSync(this.dirConfig.sslKeyPath)
|
|
||||||
) {
|
|
||||||
tlsOptions.key = fs.readFileSync(this.dirConfig.sslKeyPath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
this.dirConfig.tlsCaPath != null &&
|
|
||||||
this.dirConfig.tlsCaPath !== "" &&
|
|
||||||
fs.existsSync(this.dirConfig.tlsCaPath)
|
|
||||||
) {
|
|
||||||
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
|
|
||||||
options.tlsOptions = tlsOptions;
|
|
||||||
|
|
||||||
this.client = ldap.createClient(options);
|
|
||||||
|
|
||||||
const user =
|
|
||||||
this.dirConfig.username == null || this.dirConfig.username.trim() === ""
|
|
||||||
? null
|
|
||||||
: this.dirConfig.username;
|
|
||||||
const pass =
|
|
||||||
this.dirConfig.password == null || this.dirConfig.password.trim() === ""
|
|
||||||
? null
|
|
||||||
: this.dirConfig.password;
|
|
||||||
|
|
||||||
if (user == null || pass == null) {
|
|
||||||
reject(this.i18nService.t("usernamePasswordNotConfigured"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dirConfig.startTls && this.dirConfig.ssl) {
|
|
||||||
this.client.starttls(options.tlsOptions, undefined, (err, res) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err.message);
|
|
||||||
} else {
|
|
||||||
this.client.bind(user, pass, (err2) => {
|
|
||||||
if (err2 != null) {
|
|
||||||
reject(err2.message);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.client.bind(user, pass, (err) => {
|
if (
|
||||||
if (err != null) {
|
this.dirConfig.tlsCaPath != null &&
|
||||||
reject(err.message);
|
this.dirConfig.tlsCaPath !== "" &&
|
||||||
} else {
|
fs.existsSync(this.dirConfig.tlsCaPath)
|
||||||
resolve();
|
) {
|
||||||
}
|
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async unbind(): Promise<void> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
this.client.unbind((err) => {
|
|
||||||
if (err != null) {
|
|
||||||
reject(err);
|
|
||||||
} else {
|
|
||||||
resolve();
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
|
tlsOptions.checkServerIdentity = this.checkServerIdentityAltNames;
|
||||||
|
options.tlsOptions = tlsOptions;
|
||||||
|
|
||||||
|
this.client = new ldapts.Client(options);
|
||||||
|
|
||||||
|
const user =
|
||||||
|
this.dirConfig.username == null || this.dirConfig.username.trim() === ""
|
||||||
|
? null
|
||||||
|
: this.dirConfig.username;
|
||||||
|
const pass =
|
||||||
|
this.dirConfig.password == null || this.dirConfig.password.trim() === ""
|
||||||
|
? null
|
||||||
|
: this.dirConfig.password;
|
||||||
|
|
||||||
|
if (user == null || pass == null) {
|
||||||
|
throw new Error(this.i18nService.t("usernamePasswordNotConfigured"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dirConfig.startTls && this.dirConfig.ssl) {
|
||||||
|
await this.client.startTLS(options.tlsOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.client.bind(user, pass);
|
||||||
|
} finally {
|
||||||
|
await this.client.unbind();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bufToGuid(buf: Buffer) {
|
private bufToGuid(buf: Buffer) {
|
||||||
@@ -494,7 +444,7 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
return guid.toLowerCase();
|
return guid.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
|
private checkServerIdentityAltNames(host: string, cert: tls.PeerCertificate) {
|
||||||
// Fixes the cert representation when subject is empty and altNames are present
|
// Fixes the cert representation when subject is empty and altNames are present
|
||||||
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
|
// Required for node versions < 12.14.1 (which could be used for bwdc cli)
|
||||||
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
|
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
|
||||||
@@ -510,6 +460,6 @@ export class LdapDirectoryService implements IDirectoryService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return checkServerIdentity(host, cert);
|
return tls.checkServerIdentity(host, cert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import {
|
|
||||||
TwoFactorProviderDetails,
|
|
||||||
TwoFactorService,
|
|
||||||
} from "@/jslib/common/src/abstractions/twoFactor.service";
|
|
||||||
import { TwoFactorProviderType } from "@/jslib/common/src/enums/twoFactorProviderType";
|
|
||||||
import { IdentityTwoFactorResponse } from "@/jslib/common/src/models/response/identityTwoFactorResponse";
|
|
||||||
|
|
||||||
export class NoopTwoFactorService implements TwoFactorService {
|
|
||||||
init() {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportedProviders(win: Window): TwoFactorProviderDetails[] {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultProvider(webAuthnSupported: boolean): TwoFactorProviderType {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSelectedProvider(type: TwoFactorProviderType) {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
|
|
||||||
clearSelectedProvider() {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
|
|
||||||
setProviders(response: IdentityTwoFactorResponse) {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
|
|
||||||
clearProviders() {
|
|
||||||
// Noop
|
|
||||||
}
|
|
||||||
|
|
||||||
getProviders(): Map<TwoFactorProviderType, { [key: string]: string }> {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user