1
0
mirror of https://github.com/bitwarden/directory-connector synced 2025-12-05 23:53:21 +00:00

Compare commits

..

11 Commits

Author SHA1 Message Date
Addison Beck
ab52feec41 Delete FileUploadService 2024-07-10 17:00:33 -04:00
Addison Beck
ecce1be094 Delete PolicyService 2024-07-10 16:58:00 -04:00
Addison Beck
1d6e70d897 Delete VaultTimeoutService 2024-07-10 16:06:51 -04:00
Addison Beck
d2739b78e0 Delete ProviderService 2024-07-10 15:50:54 -04:00
Addison Beck
a9118683df Delete CollectionService 2024-07-10 15:48:37 -04:00
Addison Beck
f6e6902416 Delete FolderService 2024-07-10 15:45:57 -04:00
Addison Beck
b123ca785e Delete CipherService 2024-07-10 15:41:05 -04:00
Addison Beck
707cbb8ad6 Delete SendService 2024-07-10 15:28:59 -04:00
Addison Beck
ec45092188 Delete TotpService 2024-07-10 14:52:59 -04:00
Addison Beck
20ddfce0a8 Delete UsernameGenerationService 2024-07-10 14:47:54 -04:00
Addison Beck
1c42050c3e Delete AuditService 2024-07-10 14:36:35 -04:00
87 changed files with 6457 additions and 2564 deletions

7
.github/CODEOWNERS vendored
View File

@@ -6,3 +6,10 @@
# Default file owners.
* @bitwarden/team-admin-console-dev
# DevOps for Actions and other workflow changes.
.github/workflows @bitwarden/dept-devops
.github/secrets @bitwarden/dept-devops
# Multiple Owners
**/package.json

View File

@@ -1,34 +1,33 @@
## 🎟️ Tracking
## Type of change
<!-- Paste the link to the Jira or GitHub issue or otherwise describe / point to where this change is coming from. -->
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [ ] Other
## 📔 Objective
## Objective
<!-- Describe what the purpose of this PR is, for example what bug you're fixing or new feature you're adding. -->
<!--Describe what the purpose of this PR is. For example: what bug you're fixing or what new feature you're adding-->
## 📸 Screenshots
## Code changes
<!-- Required for any UI changes; delete if not applicable. Use fixed width images for better display. -->
<!--Explain the changes you've made to each file or major component. This should help the reviewer understand your changes-->
<!--Also refer to any related changes or PRs in other repositories-->
## ⏰ Reminders before review
- **file.ext:** Description of what was changed and why
- Contributor guidelines followed
- All formatters and local linters executed and passed
- Written new unit and / or integration tests where applicable
- Used internationalization (i18n) for all UI strings
- CI builds passed
- Communicated to DevOps any deployment requirements
- Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team
## Screenshots
## 🦮 Reviewer guidelines
<!--Required for any UI changes. Delete if not applicable-->
<!-- Suggested interactions but feel free to use (or not) as you desire! -->
## Testing requirements
- 👍 (`:+1:`) or similar for great changes
- 📝 (`:memo:`) or (`:information_source:`) for notes or general info
- ❓ (`:question:`) for questions
- 🤔 (`:thinking:`) or 💭 (`:thought_balloon:`) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
- 🎨 (`:art:`) for suggestions / improvements
- ❌ (`:x:`) or ⚠️ (`:warning:`) for more significant problems or concerns needing attention
- 🌱 (`:seedling:`) or ♻️ (`:recycle:`) for future improvements or indications of technical debt
- ⛏ (`:pick:`) for minor or nitpick changes
<!--What functionality requires testing by QA? This includes testing new behavior and regression testing-->
## Before you submit
- [ ] I have checked for **linting** errors (`npm run lint`) (required)
- [ ] I have added **unit tests** where it makes sense to do so (encouraged but not required)
- [ ] This change requires a **documentation update** (notify the documentation team)
- [ ] This change has particular **deployment requirements** (notify the DevOps team)

25
.github/renovate.json vendored
View File

@@ -1,12 +1,31 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["github>bitwarden/renovate-config"],
"extends": [
"config:base",
"github>bitwarden/renovate-config:pin-actions",
":combinePatchMinorReleases",
":dependencyDashboard",
":maintainLockFilesWeekly",
":pinAllExceptPeerDependencies",
":prConcurrentLimit10",
":rebaseStalePrs",
":separateMajorReleases",
"group:monorepos",
"schedule:weekends"
],
"enabledManagers": ["github-actions", "npm"],
"commitMessagePrefix": "[deps]:",
"commitMessageTopic": "{{depName}}",
"packageRules": [
{
"groupName": "gh minor",
"matchManagers": ["github-actions"],
"groupName": "npm minor",
"matchManagers": ["npm"],
"matchUpdateTypes": ["minor", "patch"]
},
{
"matchFileNames": ["package.json"],
"description": "Admin Console owns general dependencies",
"reviewers": ["team:team-admin-console-dev"]
}
]
}

View File

@@ -6,6 +6,8 @@ on:
push:
branches:
- "main"
- "rc"
- "hotfix-rc"
workflow_dispatch: {}
jobs:
@@ -576,20 +578,12 @@ jobs:
- name: Install Node dependencies
run: npm install
- name: Set up private auth key
run: |
mkdir ~/private_keys
cat << EOF > ~/private_keys/AuthKey_UFD296548T.p8
${{ secrets.APP_STORE_CONNECT_AUTH_KEY }}
EOF
- name: Build application
run: npm run dist:mac
env:
APP_STORE_CONNECT_TEAM_ISSUER: ${{ secrets.APP_STORE_CONNECT_TEAM_ISSUER }}
APP_STORE_CONNECT_AUTH_KEY: UFD296548T
APP_STORE_CONNECT_AUTH_KEY_PATH: ~/private_keys/AuthKey_UFD296548T.p8
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
CSC_FOR_PULL_REQUEST: true
- name: Upload .zip artifact
@@ -635,7 +629,7 @@ jobs:
- macos-gui
steps:
- name: Check if any job failed
if: github.ref == 'refs/heads/main' && contains(needs.*.result, 'failure')
if: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc') && contains(needs.*.result, 'failure')
run: exit 1
- name: Login to Azure - CI subscription

53
.github/workflows/cleanup-rc-branch.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
---
name: Cleanup RC Branch
on:
push:
tags:
- v**
jobs:
delete-rc:
name: Delete RC Branch
runs-on: ubuntu-22.04
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve bot secrets
id: retrieve-bot-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: bitwarden-ci
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
- name: Check if a RC branch exists
id: branch-check
run: |
hotfix_rc_branch_check=$(git ls-remote --heads origin hotfix-rc | wc -l)
rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${hotfix_rc_branch_check}" -gt 0 ]]; then
echo "hotfix-rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=hotfix-rc" >> $GITHUB_OUTPUT
elif [[ "${rc_branch_check}" -gt 0 ]]; then
echo "rc branch exists." | tee -a $GITHUB_STEP_SUMMARY
echo "name=rc" >> $GITHUB_OUTPUT
fi
- name: Delete RC branch
env:
BRANCH_NAME: ${{ steps.branch-check.outputs.name }}
run: |
if ! [[ -z "$BRANCH_NAME" ]]; then
git push --quiet origin --delete $BRANCH_NAME
echo "Deleted $BRANCH_NAME branch." | tee -a $GITHUB_STEP_SUMMARY
fi

View File

@@ -27,9 +27,9 @@ jobs:
- name: Branch check
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
run: |
if [[ "$GITHUB_REF" != "refs/heads/main" ]]; then
if [[ "$GITHUB_REF" != "refs/heads/rc" ]] && [[ "$GITHUB_REF" != "refs/heads/hotfix-rc" ]]; then
echo "==================================="
echo "[!] Can only release from the 'main' branch"
echo "[!] Can only release from the 'rc' or 'hotfix-rc' branches"
echo "==================================="
exit 1
fi
@@ -47,6 +47,16 @@ jobs:
runs-on: ubuntu-22.04
needs: setup
steps:
- name: Create GitHub deployment
uses: chrnorm/deployment-action@55729fcebec3d284f60f5bcabbd8376437d696b1 # v2.0.7
id: deployment
with:
token: '${{ secrets.GITHUB_TOKEN }}'
initial-status: 'in_progress'
environment: 'production'
description: 'Deployment ${{ needs.setup.outputs.release-version }} from branch ${{ github.ref_name }}'
task: release
- name: Download all artifacts
if: ${{ github.event.inputs.release_type != 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
@@ -55,7 +65,7 @@ jobs:
workflow_conclusion: success
branch: ${{ github.ref_name }}
- name: Dry Run - Download all artifacts
- name: Download all artifacts
if: ${{ github.event.inputs.release_type == 'Dry Run' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
@@ -91,3 +101,19 @@ jobs:
body: "<insert release notes here>"
token: ${{ secrets.GITHUB_TOKEN }}
draft: true
- name: Update deployment status to Success
if: ${{ success() }}
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'success'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
- name: Update deployment status to Failure
if: ${{ failure() }}
uses: chrnorm/deployment-status@9a72af4586197112e0491ea843682b5dc280d806 # v2.0.3
with:
token: '${{ secrets.GITHUB_TOKEN }}'
state: 'failure'
deployment-id: ${{ steps.deployment.outputs.deployment_id }}

View File

@@ -1,78 +0,0 @@
name: Scan
on:
workflow_dispatch:
push:
branches:
- "main"
pull_request_target:
types: [opened, synchronize]
jobs:
check-run:
name: Check PR run
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
sast:
name: SAST scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
security-events: write
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@6c56658230f79c227a55120e9b24845d574d5225 # 2.0.31
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
project_name: ${{ github.repository }}
cx_tenant: ${{ secrets.CHECKMARX_TENANT }}
base_uri: https://ast.checkmarx.net/
cx_client_id: ${{ secrets.CHECKMARX_CLIENT_ID }}
cx_client_secret: ${{ secrets.CHECKMARX_SECRET }}
additional_params: |
--report-format sarif \
--filter "state=TO_VERIFY;PROPOSED_NOT_EXPLOITABLE;CONFIRMED;URGENT" \
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@1b1aada464948af03b950897e5eb522f92603cc2 # v3.24.9
with:
sarif_file: cx_result.sarif
quality:
name: Quality scan
runs-on: ubuntu-22.04
needs: check-run
permissions:
contents: read
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with SonarCloud
uses: sonarsource/sonarcloud-github-action@eb211723266fe8e83102bac7361f0a05c3ac1d1b # v3.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: >
-Dsonar.organization=${{ github.repository_owner }}
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }}
-Dsonar.tests=.
-Dsonar.sources=.
-Dsonar.test.inclusions=**/*.spec.ts
-Dsonar.exclusions=**/*.spec.ts

View File

@@ -1,46 +1,28 @@
name: Testing
---
name: Run tests
on:
workflow_dispatch:
push:
branches:
- "main"
pull_request:
- "rc"
- "hotfix-rc-*"
pull_request: {}
defaults:
run:
shell: bash
jobs:
check-test-secrets:
name: Check for test secrets
runs-on: ubuntu-22.04
outputs:
available: ${{ steps.check-test-secrets.outputs.available }}
permissions:
contents: read
steps:
- name: Check
id: check-test-secrets
run: |
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
echo "available=true" >> $GITHUB_OUTPUT;
else
echo "available=false" >> $GITHUB_OUTPUT;
fi
testing:
test:
name: Run tests
if: ${{ startsWith(github.head_ref, 'version_bump_') == false }}
runs-on: ubuntu-22.04
needs: check-test-secrets
permissions:
checks: write
contents: read
pull-requests: write
steps:
- name: Check out repo
- name: Checkout repo
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Get Node version
- name: Get Node Version
id: retrieve-node-version
run: |
NODE_NVMRC=$(cat .nvmrc)
@@ -54,6 +36,11 @@ jobs:
cache-dependency-path: '**/package-lock.json'
node-version: ${{ steps.retrieve-node-version.outputs.node_version }}
- name: Print environment
run: |
node --version
npm --version
- name: Install Node dependencies
run: npm ci
@@ -64,25 +51,4 @@ jobs:
run: npm run test:types --coverage
- name: Run tests
run: npm run test --coverage
- name: Report test results
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
with:
name: Test Results
path: "junit.xml"
reporter: jest-junit
fail-on-error: true
- name: Upload coverage to codecov.io
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
- name: Upload results to codecov.io
uses: codecov/test-results-action@1b5b448b98e58ba90d1a1a1d9fcb72ca2263be46 # v1.0.0
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
run: npm run test

View File

@@ -8,6 +8,10 @@ on:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
enable_slack_notification:
description: "Enable Slack notifications for upcoming release?"
default: false
@@ -36,6 +40,18 @@ jobs:
- name: Checkout Branch
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: main
- name: Check if RC branch exists
if: ${{ inputs.cut_rc_branch == true }}
run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -48,7 +64,8 @@ jobs:
with:
keyvault: "bitwarden-ci"
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
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
@@ -149,19 +166,11 @@ jobs:
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
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
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: |
@@ -192,7 +201,7 @@ jobs:
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
@@ -203,3 +212,34 @@ jobs:
version: ${{ steps.set-final-version-output.outputs.version }}
project: ${{ github.repository }}
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps:
- name: Checkout Branch
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
ref: main
- name: Verify version has been updated
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(cat package.json | jq -r '.version')
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc

1
.gitignore vendored
View File

@@ -26,7 +26,6 @@ npm-debug.log
# Build directories
dist
build
build-cli
.angular/cache
# Testing

View File

@@ -11,14 +11,6 @@ module.exports = {
// ...angularPreset,
preset: "jest-preset-angular",
reporters: ["default", "jest-junit"],
collectCoverage: true,
// Ensure we collect coverage from files without tests
collectCoverageFrom: ["src/**/*.ts"],
coverageReporters: ["html", "lcov"],
coverageDirectory: "coverage",
testEnvironment: "jsdom",
testMatch: ["**/+(*.)+(spec).+(ts)"],

View File

@@ -0,0 +1,34 @@
import { Injectable } from "@angular/core";
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from "@angular/router";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { MessagingService } from "@/jslib/common/src/abstractions/messaging.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
@Injectable()
export class AuthGuardService {
constructor(
private router: Router,
private messagingService: MessagingService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
) {}
async canActivate(route: ActivatedRouteSnapshot, routerState: RouterStateSnapshot) {
const isAuthed = await this.stateService.getIsAuthenticated();
if (!isAuthed) {
this.messagingService.send("authBlocked");
return false;
}
if (
!routerState.url.includes("remove-password") &&
(await this.keyConnectorService.getConvertAccountRequired())
) {
this.router.navigate(["/remove-password"]);
return false;
}
return true;
}
}

View File

@@ -2,69 +2,106 @@ import { LOCALE_ID, NgModule } from "@angular/core";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.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 { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { EnvironmentService as EnvironmentServiceAbstraction } from "@/jslib/common/src/abstractions/environment.service";
import { EventService as EventServiceAbstraction } from "@/jslib/common/src/abstractions/event.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 { MessagingService as MessagingServiceAbstraction } from "@/jslib/common/src/abstractions/messaging.service";
import { NotificationsService as NotificationsServiceAbstraction } from "@/jslib/common/src/abstractions/notifications.service";
import { OrganizationService as OrganizationServiceAbstraction } from "@/jslib/common/src/abstractions/organization.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@/jslib/common/src/abstractions/passwordGeneration.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib/common/src/abstractions/platformUtils.service";
import { SearchService as SearchServiceAbstraction } from "@/jslib/common/src/abstractions/search.service";
import { SettingsService as SettingsServiceAbstraction } from "@/jslib/common/src/abstractions/settings.service";
import { StateService as StateServiceAbstraction } from "@/jslib/common/src/abstractions/state.service";
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.service";
import { SyncService as SyncServiceAbstraction } from "@/jslib/common/src/abstractions/sync.service";
import { TokenService as TokenServiceAbstraction } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@/jslib/common/src/abstractions/twoFactor.service";
import { UserVerificationService as UserVerificationServiceAbstraction } from "@/jslib/common/src/abstractions/userVerification.service";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
import { Account } from "@/jslib/common/src/models/domain/account";
import { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { ApiService } from "@/jslib/common/src/services/api.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 { CryptoService } from "@/jslib/common/src/services/crypto.service";
import { EnvironmentService } from "@/jslib/common/src/services/environment.service";
import { EventService } from "@/jslib/common/src/services/event.service";
import { KeyConnectorService } from "@/jslib/common/src/services/keyConnector.service";
import { NotificationsService } from "@/jslib/common/src/services/notifications.service";
import { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
import { SearchService } from "@/jslib/common/src/services/search.service";
import { SettingsService } from "@/jslib/common/src/services/settings.service";
import { StateService } from "@/jslib/common/src/services/state.service";
import { StateMigrationService } from "@/jslib/common/src/services/stateMigration.service";
import { SyncService } from "@/jslib/common/src/services/sync.service";
import { TokenService } from "@/jslib/common/src/services/token.service";
import { TwoFactorService } from "@/jslib/common/src/services/twoFactor.service";
import { UserVerificationService } from "@/jslib/common/src/services/userVerification.service";
import {
SafeInjectionToken,
SECURE_STORAGE,
WINDOW,
} from "../../../../src/app/services/injection-tokens";
import { SafeProvider, safeProvider } from "../../../../src/app/services/safe-provider";
import { AuthGuardService } from "./auth-guard.service";
import { BroadcasterService } from "./broadcaster.service";
import { LockGuardService } from "./lock-guard.service";
import { ModalService } from "./modal.service";
import { PasswordRepromptService } from "./passwordReprompt.service";
import { UnauthGuardService } from "./unauth-guard.service";
import { ValidationService } from "./validation.service";
@NgModule({
declarations: [],
providers: [
safeProvider({ provide: WINDOW, useValue: window }),
safeProvider({
provide: LOCALE_ID as SafeInjectionToken<string>,
{ provide: "WINDOW", useValue: window },
{
provide: LOCALE_ID,
useFactory: (i18nService: I18nServiceAbstraction) => i18nService.translationLocale,
deps: [I18nServiceAbstraction],
}),
safeProvider(ValidationService),
safeProvider(ModalService),
safeProvider({
},
ValidationService,
AuthGuardService,
UnauthGuardService,
LockGuardService,
ModalService,
{
provide: AppIdServiceAbstraction,
useClass: AppIdService,
deps: [StorageServiceAbstraction],
}),
safeProvider({ provide: LogService, useFactory: () => new ConsoleLogService(false), deps: [] }),
safeProvider({
},
{
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
],
},
{ provide: LogService, useFactory: () => new ConsoleLogService(false) },
{
provide: EnvironmentServiceAbstraction,
useClass: EnvironmentService,
deps: [StateServiceAbstraction],
}),
safeProvider({
provide: TokenServiceAbstraction,
useClass: TokenService,
deps: [StateServiceAbstraction],
}),
safeProvider({
},
{ provide: TokenServiceAbstraction, useClass: TokenService, deps: [StateServiceAbstraction] },
{
provide: CryptoServiceAbstraction,
useClass: CryptoService,
deps: [
@@ -73,8 +110,13 @@ import { ValidationService } from "./validation.service";
LogService,
StateServiceAbstraction,
],
}),
safeProvider({
},
{
provide: PasswordGenerationServiceAbstraction,
useClass: PasswordGenerationService,
deps: [CryptoServiceAbstraction, StateServiceAbstraction],
},
{
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
@@ -97,13 +139,48 @@ import { ValidationService } from "./validation.service";
MessagingServiceAbstraction,
AppIdServiceAbstraction,
],
}),
safeProvider({
provide: BroadcasterServiceAbstraction,
useClass: BroadcasterService,
useAngularDecorators: true,
}),
safeProvider({
},
{
provide: SyncServiceAbstraction,
useFactory: (
apiService: ApiServiceAbstraction,
settingsService: SettingsServiceAbstraction,
cryptoService: CryptoServiceAbstraction,
messagingService: MessagingServiceAbstraction,
logService: LogService,
keyConnectorService: KeyConnectorServiceAbstraction,
stateService: StateServiceAbstraction,
organizationService: OrganizationServiceAbstraction,
) =>
new SyncService(
apiService,
settingsService,
cryptoService,
messagingService,
logService,
keyConnectorService,
stateService,
organizationService,
async (expired: boolean) => messagingService.send("logout", { expired: expired }),
),
deps: [
ApiServiceAbstraction,
SettingsServiceAbstraction,
CryptoServiceAbstraction,
MessagingServiceAbstraction,
LogService,
KeyConnectorServiceAbstraction,
StateServiceAbstraction,
OrganizationServiceAbstraction,
],
},
{ provide: BroadcasterServiceAbstraction, useClass: BroadcasterService },
{
provide: SettingsServiceAbstraction,
useClass: SettingsService,
deps: [StateServiceAbstraction],
},
{
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -120,12 +197,12 @@ import { ValidationService } from "./validation.service";
),
deps: [
StorageServiceAbstraction,
SECURE_STORAGE,
"SECURE_STORAGE",
LogService,
StateMigrationServiceAbstraction,
],
}),
safeProvider({
},
{
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -136,8 +213,82 @@ import { ValidationService } from "./validation.service";
secureStorageService,
new StateFactory(GlobalState, Account),
),
deps: [StorageServiceAbstraction, SECURE_STORAGE],
}),
] satisfies SafeProvider[],
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
provide: SearchServiceAbstraction,
useClass: SearchService,
deps: [LogService, I18nServiceAbstraction],
},
{
provide: NotificationsServiceAbstraction,
useFactory: (
syncService: SyncServiceAbstraction,
appIdService: AppIdServiceAbstraction,
apiService: ApiServiceAbstraction,
environmentService: EnvironmentServiceAbstraction,
messagingService: MessagingServiceAbstraction,
logService: LogService,
stateService: StateServiceAbstraction,
) =>
new NotificationsService(
syncService,
appIdService,
apiService,
environmentService,
async () => messagingService.send("logout", { expired: true }),
logService,
stateService,
),
deps: [
SyncServiceAbstraction,
AppIdServiceAbstraction,
ApiServiceAbstraction,
EnvironmentServiceAbstraction,
MessagingServiceAbstraction,
LogService,
StateServiceAbstraction,
],
},
{
provide: EventServiceAbstraction,
useClass: EventService,
deps: [
ApiServiceAbstraction,
StateServiceAbstraction,
LogService,
OrganizationServiceAbstraction,
],
},
{
provide: KeyConnectorServiceAbstraction,
useClass: KeyConnectorService,
deps: [
StateServiceAbstraction,
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
LogService,
OrganizationServiceAbstraction,
CryptoFunctionServiceAbstraction,
],
},
{
provide: UserVerificationServiceAbstraction,
useClass: UserVerificationService,
deps: [CryptoServiceAbstraction, I18nServiceAbstraction, ApiServiceAbstraction],
},
{ provide: PasswordRepromptServiceAbstraction, useClass: PasswordRepromptService },
{
provide: OrganizationServiceAbstraction,
useClass: OrganizationService,
deps: [StateServiceAbstraction],
},
{
provide: TwoFactorServiceAbstraction,
useClass: TwoFactorService,
deps: [I18nServiceAbstraction, PlatformUtilsServiceAbstraction],
},
],
})
export class JslibServicesModule {}

View File

@@ -0,0 +1,23 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
@Injectable()
export class LockGuardService {
protected homepage = "vault";
protected loginpage = "login";
constructor(
private router: Router,
private stateService: StateService,
) {}
async canActivate() {
const redirectUrl = (await this.stateService.getIsAuthenticated())
? [this.homepage]
: [this.loginpage];
this.router.navigate(redirectUrl);
return false;
}
}

View File

@@ -143,7 +143,7 @@ export class ModalService {
dialogEl.style.zIndex = `${this.modalCount}050`;
const modals = Array.from(
el.querySelectorAll('.modal-backdrop, .modal *[data-bs-dismiss="modal"]'),
el.querySelectorAll('.modal-backdrop, .modal *[data-dismiss="modal"]'),
);
for (const closeElement of modals) {
closeElement.addEventListener("click", () => {

View File

@@ -0,0 +1,45 @@
import { Injectable } from "@angular/core";
import { KeyConnectorService } from "@/jslib/common/src/abstractions/keyConnector.service";
import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@/jslib/common/src/abstractions/passwordReprompt.service";
import { PasswordRepromptComponent } from "../components/password-reprompt.component";
import { ModalService } from "./modal.service";
/**
* Used to verify the user's Master Password for the "Master Password Re-prompt" feature only.
* See UserVerificationService for any other situation where you need to verify the user's identity.
*/
@Injectable()
export class PasswordRepromptService implements PasswordRepromptServiceAbstraction {
protected component = PasswordRepromptComponent;
constructor(
private modalService: ModalService,
private keyConnectorService: KeyConnectorService,
) {}
protectedFields() {
return ["TOTP", "Password", "H_Field", "Card Number", "Security Code"];
}
async showPasswordPrompt() {
if (!(await this.enabled())) {
return true;
}
const ref = this.modalService.open(this.component, { allowMultipleModals: true });
if (ref == null) {
return false;
}
const result = await ref.onClosedPromise();
return result === true;
}
async enabled() {
return !(await this.keyConnectorService.getUsesKeyConnector());
}
}

View File

@@ -0,0 +1,22 @@
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
@Injectable()
export class UnauthGuardService {
protected homepage = "vault";
constructor(
private router: Router,
private stateService: StateService,
) {}
async canActivate() {
const isAuthed = await this.stateService.getIsAuthenticated();
if (isAuthed) {
this.router.navigate([this.homepage]);
return false;
}
return true;
}
}

View File

@@ -0,0 +1,114 @@
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);
});
});

View File

@@ -0,0 +1,288 @@
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
);
}),
);
});
});
});

View File

@@ -0,0 +1,110 @@
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);
});
});

View File

@@ -0,0 +1,127 @@
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);
});
});

View File

@@ -0,0 +1,25 @@
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;
}

View File

@@ -0,0 +1,7 @@
import { EventType } from "../enums/eventType";
export abstract class EventService {
collect: (eventType: EventType, cipherId?: string, uploadImmediately?: boolean) => Promise<any>;
uploadEvents: (userId?: string) => Promise<any>;
clearEvents: (userId?: string) => Promise<any>;
}

View File

@@ -0,0 +1,19 @@
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>;
}

View File

@@ -0,0 +1,6 @@
export abstract class NotificationsService {
init: () => Promise<void>;
updateConnection: (sync?: boolean) => Promise<void>;
reconnectFromActivity: () => Promise<void>;
disconnectFromInactivity: () => Promise<void>;
}

View File

@@ -0,0 +1,11 @@
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>;
}

View File

@@ -0,0 +1,19 @@
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]>;
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;
}

View File

@@ -0,0 +1,5 @@
export abstract class PasswordRepromptService {
protectedFields: () => string[];
showPasswordPrompt: () => Promise<boolean>;
enabled: () => Promise<boolean>;
}

View File

@@ -0,0 +1,16 @@
import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView";
export abstract class SearchService {
indexedEntityId?: string = null;
clearIndex: () => void;
isSearchable: (query: string) => boolean;
indexCiphers: (indexedEntityGuid?: string, ciphersToIndex?: CipherView[]) => Promise<void>;
searchCiphers: (
query: string,
filter?: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[],
ciphers?: CipherView[],
) => Promise<CipherView[]>;
searchCiphersBasic: (ciphers: CipherView[], query: string, deleted?: boolean) => CipherView[];
searchSends: (sends: SendView[], query: string) => SendView[];
}

View File

@@ -0,0 +1,6 @@
export abstract class SettingsService {
clearCache: () => Promise<void>;
getEquivalentDomains: () => Promise<any>;
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
clear: (userId?: string) => Promise<void>;
}

View File

@@ -0,0 +1,10 @@
import {
} from "../models/response/notificationResponse";
export abstract class SyncService {
syncInProgress: boolean;
getLastSync: () => Promise<Date>;
setLastSync: (date: Date, userId?: string) => Promise<any>;
fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise<boolean>;
}

View File

@@ -0,0 +1,6 @@
export abstract class SystemService {
startProcessReload: () => Promise<void>;
cancelProcessReload: () => void;
clearClipboard: (clipboardValue: string, timeoutMs?: number) => Promise<void>;
clearPendingClipboard: () => Promise<any>;
}

View File

@@ -0,0 +1,23 @@
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 }>;
}

View File

@@ -0,0 +1,12 @@
import { SecretVerificationRequest } from "../models/request/secretVerificationRequest";
import { Verification } from "../types/verification";
export abstract class UserVerificationService {
buildRequest: <T extends SecretVerificationRequest>(
verification: Verification,
requestClass?: new () => T,
alreadyHashed?: boolean,
) => Promise<T>;
verifyUser: (verification: Verification) => Promise<boolean>;
requestOTP: () => Promise<void>;
}

View File

@@ -0,0 +1,5 @@
export enum AuthenticationType {
Password = 0,
Sso = 1,
Api = 2,
}

View File

@@ -0,0 +1,70 @@
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);
}
}

View File

@@ -0,0 +1,170 @@
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);
}
}
}

View File

@@ -0,0 +1,95 @@
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();
}
}

View File

@@ -0,0 +1,70 @@
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();
}
}

View File

@@ -3,6 +3,7 @@ import { I18nService } from "../abstractions/i18n.service";
import * as tldjs from "tldjs";
const nodeURL = typeof window === "undefined" ? require("url") : null;
export class Utils {

View File

@@ -0,0 +1,17 @@
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;
}
}

View File

@@ -0,0 +1,34 @@
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,
) {}
}

View File

@@ -0,0 +1,198 @@
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);
}
}
}

View File

@@ -0,0 +1,91 @@
import { ApiService } from "../abstractions/api.service";
import { EventService as EventServiceAbstraction } from "../abstractions/event.service";
import { LogService } from "../abstractions/log.service";
import { OrganizationService } from "../abstractions/organization.service";
import { StateService } from "../abstractions/state.service";
import { EventType } from "../enums/eventType";
import { EventData } from "../models/data/eventData";
import { EventRequest } from "../models/request/eventRequest";
export class EventService implements EventServiceAbstraction {
private inited = false;
constructor(
private apiService: ApiService,
private stateService: StateService,
private logService: LogService,
private organizationService: OrganizationService,
) {}
init(checkOnInterval: boolean) {
if (this.inited) {
return;
}
this.inited = true;
if (checkOnInterval) {
this.uploadEvents();
setInterval(() => this.uploadEvents(), 60 * 1000); // check every 60 seconds
}
}
async collect(
eventType: EventType,
cipherId: string = null,
uploadImmediately = false,
): Promise<any> {
const authed = await this.stateService.getIsAuthenticated();
if (!authed) {
return;
}
const organizations = await this.organizationService.getAll();
if (organizations == null) {
return;
}
const orgIds = new Set<string>(organizations.filter((o) => o.useEvents).map((o) => o.id));
if (orgIds.size === 0) {
return;
}
let eventCollection = await this.stateService.getEventCollection();
if (eventCollection == null) {
eventCollection = [];
}
const event = new EventData();
event.type = eventType;
event.cipherId = cipherId;
event.date = new Date().toISOString();
eventCollection.push(event);
await this.stateService.setEventCollection(eventCollection);
if (uploadImmediately) {
await this.uploadEvents();
}
}
async uploadEvents(userId?: string): Promise<any> {
const authed = await this.stateService.getIsAuthenticated({ userId: userId });
if (!authed) {
return;
}
const eventCollection = await this.stateService.getEventCollection({ userId: userId });
if (eventCollection == null || eventCollection.length === 0) {
return;
}
const request = eventCollection.map((e) => {
const req = new EventRequest();
req.type = e.type;
req.cipherId = e.cipherId;
req.date = e.date;
return req;
});
try {
await this.apiService.postEventsCollect(request);
this.clearEvents(userId);
} catch (e) {
this.logService.error(e);
}
}
async clearEvents(userId?: string): Promise<any> {
await this.stateService.setEventCollection(null, { userId: userId });
}
}

View File

@@ -0,0 +1,134 @@
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();
}
}

View File

@@ -0,0 +1,191 @@
import * as signalR from "@microsoft/signalr";
import * as signalRMsgPack from "@microsoft/signalr-protocol-msgpack";
import { ApiService } from "../abstractions/api.service";
import { AppIdService } from "../abstractions/appId.service";
import { EnvironmentService } from "../abstractions/environment.service";
import { LogService } from "../abstractions/log.service";
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
import { StateService } from "../abstractions/state.service";
import { SyncService } from "../abstractions/sync.service";
import { NotificationType } from "../enums/notificationType";
import {
NotificationResponse,
} from "../models/response/notificationResponse";
export class NotificationsService implements NotificationsServiceAbstraction {
private signalrConnection: signalR.HubConnection;
private url: string;
private connected = false;
private inited = false;
private inactive = false;
private reconnectTimer: any = null;
constructor(
private syncService: SyncService,
private appIdService: AppIdService,
private apiService: ApiService,
private environmentService: EnvironmentService,
private logoutCallback: () => Promise<void>,
private logService: LogService,
private stateService: StateService,
) {
this.environmentService.urls.subscribe(() => {
if (!this.inited) {
return;
}
this.init();
});
}
async init(): Promise<void> {
this.inited = false;
this.url = this.environmentService.getNotificationsUrl();
// Set notifications server URL to `https://-` to effectively disable communication
// with the notifications server from the client app
if (this.url === "https://-") {
return;
}
if (this.signalrConnection != null) {
this.signalrConnection.off("ReceiveMessage");
this.signalrConnection.off("Heartbeat");
await this.signalrConnection.stop();
this.connected = false;
this.signalrConnection = null;
}
this.signalrConnection = new signalR.HubConnectionBuilder()
.withUrl(this.url + "/hub", {
accessTokenFactory: () => this.apiService.getActiveBearerToken(),
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
})
.withHubProtocol(new signalRMsgPack.MessagePackHubProtocol() as signalR.IHubProtocol)
// .configureLogging(signalR.LogLevel.Trace)
.build();
this.signalrConnection.on("ReceiveMessage", (data: any) =>
this.processNotification(new NotificationResponse(data)),
);
// eslint-disable-next-line
this.signalrConnection.on("Heartbeat", (data: any) => {
/*console.log('Heartbeat!');*/
});
this.signalrConnection.onclose(() => {
this.connected = false;
this.reconnect(true);
});
this.inited = true;
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(false);
}
}
async updateConnection(sync = false): Promise<void> {
if (!this.inited) {
return;
}
try {
if (await this.isAuthedAndUnlocked()) {
await this.reconnect(sync);
} else {
await this.signalrConnection.stop();
}
} catch (e) {
this.logService.error(e.toString());
}
}
async reconnectFromActivity(): Promise<void> {
this.inactive = false;
if (this.inited && !this.connected) {
await this.reconnect(true);
}
}
async disconnectFromInactivity(): Promise<void> {
this.inactive = true;
if (this.inited && this.connected) {
await this.signalrConnection.stop();
}
}
private async processNotification(notification: NotificationResponse) {
const appId = await this.appIdService.getAppId();
if (notification == null || notification.contextId === appId) {
return;
}
const isAuthenticated = await this.stateService.getIsAuthenticated();
const payloadUserId = notification.payload.userId || notification.payload.UserId;
const myUserId = await this.stateService.getUserId();
if (isAuthenticated && payloadUserId != null && payloadUserId !== myUserId) {
return;
}
switch (notification.type) {
case NotificationType.SyncVault:
case NotificationType.SyncCiphers:
case NotificationType.SyncSettings:
if (isAuthenticated) {
await this.syncService.fullSync(false);
}
break;
case NotificationType.SyncOrgKeys:
if (isAuthenticated) {
await this.syncService.fullSync(true);
// Stop so a reconnect can be made
await this.signalrConnection.stop();
}
break;
case NotificationType.LogOut:
if (isAuthenticated) {
this.logoutCallback();
}
break;
default:
break;
}
}
private async reconnect(sync: boolean) {
if (this.reconnectTimer != null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.connected || !this.inited || this.inactive) {
return;
}
const authedAndUnlocked = await this.isAuthedAndUnlocked();
if (!authedAndUnlocked) {
return;
}
try {
await this.signalrConnection.start();
this.connected = true;
if (sync) {
await this.syncService.fullSync(false);
}
} catch (e) {
this.logService.error(e);
}
if (!this.connected) {
this.reconnectTimer = setTimeout(() => this.reconnect(sync), this.random(120000, 300000));
}
}
private async isAuthedAndUnlocked() {
return await this.stateService.getIsAuthenticated()
}
private random(min: number, max: number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
}

View File

@@ -0,0 +1,55 @@
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;
}
}

View File

@@ -0,0 +1,429 @@
import * as zxcvbn from "zxcvbn";
import { CryptoService } from "../abstractions/crypto.service";
import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "../abstractions/passwordGeneration.service";
import { StateService } from "../abstractions/state.service";
import { EEFLongWordList } from "../misc/wordlist";
import { EncString } from "../models/domain/encString";
import { GeneratedPasswordHistory } from "../models/domain/generatedPasswordHistory";
import { PasswordGeneratorPolicyOptions } from "../models/domain/passwordGeneratorPolicyOptions";
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 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]> {
return [options, new PasswordGeneratorPolicyOptions()];
}
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;
}
}
}

View File

@@ -0,0 +1,276 @@
import * as lunr from "lunr";
import { I18nService } from "../abstractions/i18n.service";
import { LogService } from "../abstractions/log.service";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { CipherType } from "../enums/cipherType";
import { FieldType } from "../enums/fieldType";
import { UriMatchType } from "../enums/uriMatchType";
import { CipherView } from "../models/view/cipherView";
import { SendView } from "../models/view/sendView";
export class SearchService implements SearchServiceAbstraction {
indexedEntityId?: string = null;
private indexing = false;
private index: lunr.Index = null;
private searchableMinLength = 2;
constructor(
private logService: LogService,
private i18nService: I18nService,
) {
if (["zh-CN", "zh-TW"].indexOf(i18nService.locale) !== -1) {
this.searchableMinLength = 1;
}
}
clearIndex(): void {
this.indexedEntityId = null;
this.index = null;
}
isSearchable(query: string): boolean {
const notSearchable =
query == null ||
(this.index == null && query.length < this.searchableMinLength) ||
(this.index != null && query.length < this.searchableMinLength && query.indexOf(">") !== 0);
return !notSearchable;
}
async indexCiphers(indexedEntityId?: string, ciphers?: CipherView[]): Promise<void> {
if (this.indexing) {
return;
}
this.logService.time("search indexing");
this.indexing = true;
this.indexedEntityId = indexedEntityId;
this.index = null;
const builder = new lunr.Builder();
builder.ref("id");
builder.field("shortid", { boost: 100, extractor: (c: CipherView) => c.id.substr(0, 8) });
builder.field("name", { boost: 10 });
builder.field("subtitle", {
boost: 5,
extractor: (c: CipherView) => {
if (c.subTitle != null && c.type === CipherType.Card) {
return c.subTitle.replace(/\*/g, "");
}
return c.subTitle;
},
});
builder.field("notes");
builder.field("login.username", {
extractor: (c: CipherView) =>
c.type === CipherType.Login && c.login != null ? c.login.username : null,
});
builder.field("login.uris", { boost: 2, extractor: (c: CipherView) => this.uriExtractor(c) });
builder.field("fields", { extractor: (c: CipherView) => this.fieldExtractor(c, false) });
builder.field("fields_joined", { extractor: (c: CipherView) => this.fieldExtractor(c, true) });
builder.field("attachments", {
extractor: (c: CipherView) => this.attachmentExtractor(c, false),
});
builder.field("attachments_joined", {
extractor: (c: CipherView) => this.attachmentExtractor(c, true),
});
builder.field("organizationid", { extractor: (c: CipherView) => c.organizationId });
ciphers.forEach((c) => builder.add(c));
this.index = builder.build();
this.indexing = false;
this.logService.timeEnd("search indexing");
}
async searchCiphers(
query: string,
filter: ((cipher: CipherView) => boolean) | ((cipher: CipherView) => boolean)[] = null,
ciphers: CipherView[] = null,
): Promise<CipherView[]> {
const results: CipherView[] = [];
if (query != null) {
query = query.trim().toLowerCase();
}
if (query === "") {
query = null;
}
if (filter != null && Array.isArray(filter) && filter.length > 0) {
ciphers = ciphers.filter((c) => filter.every((f) => f == null || f(c)));
} else if (filter != null) {
ciphers = ciphers.filter(filter as (cipher: CipherView) => boolean);
}
if (!this.isSearchable(query)) {
return ciphers;
}
if (this.indexing) {
await new Promise((r) => setTimeout(r, 250));
if (this.indexing) {
await new Promise((r) => setTimeout(r, 500));
}
}
const index = this.getIndexForSearch();
if (index == null) {
// Fall back to basic search if index is not available
return this.searchCiphersBasic(ciphers, query);
}
const ciphersMap = new Map<string, CipherView>();
ciphers.forEach((c) => ciphersMap.set(c.id, c));
let searchResults: lunr.Index.Result[] = null;
const isQueryString = query != null && query.length > 1 && query.indexOf(">") === 0;
if (isQueryString) {
try {
searchResults = index.search(query.substr(1).trim());
} catch (e) {
this.logService.error(e);
}
} else {
const soWild = lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING;
searchResults = index.query((q) => {
lunr.tokenizer(query).forEach((token) => {
const t = token.toString();
q.term(t, { fields: ["name"], wildcard: soWild });
q.term(t, { fields: ["subtitle"], wildcard: soWild });
q.term(t, { fields: ["login.uris"], wildcard: soWild });
q.term(t, {});
});
});
}
if (searchResults != null) {
searchResults.forEach((r) => {
if (ciphersMap.has(r.ref)) {
results.push(ciphersMap.get(r.ref));
}
});
}
return results;
}
searchCiphersBasic(ciphers: CipherView[], query: string, deleted = false) {
query = query.trim().toLowerCase();
return ciphers.filter((c) => {
if (deleted !== c.isDeleted) {
return false;
}
if (c.name != null && c.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (query.length >= 8 && c.id.startsWith(query)) {
return true;
}
if (c.subTitle != null && c.subTitle.toLowerCase().indexOf(query) > -1) {
return true;
}
if (c.login && c.login.uri != null && c.login.uri.toLowerCase().indexOf(query) > -1) {
return true;
}
return false;
});
}
searchSends(sends: SendView[], query: string) {
query = query.trim().toLocaleLowerCase();
return sends.filter((s) => {
if (s.name != null && s.name.toLowerCase().indexOf(query) > -1) {
return true;
}
if (
query.length >= 8 &&
(s.id.startsWith(query) ||
s.accessId.toLocaleLowerCase().startsWith(query) ||
(s.file?.id != null && s.file.id.startsWith(query)))
) {
return true;
}
if (s.notes != null && s.notes.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.text?.text != null && s.text.text.toLowerCase().indexOf(query) > -1) {
return true;
}
if (s.file?.fileName != null && s.file.fileName.toLowerCase().indexOf(query) > -1) {
return true;
}
});
}
getIndexForSearch(): lunr.Index {
return this.index;
}
private fieldExtractor(c: CipherView, joined: boolean) {
if (!c.hasFields) {
return null;
}
let fields: string[] = [];
c.fields.forEach((f) => {
if (f.name != null) {
fields.push(f.name);
}
if (f.type === FieldType.Text && f.value != null) {
fields.push(f.value);
}
});
fields = fields.filter((f) => f.trim() !== "");
if (fields.length === 0) {
return null;
}
return joined ? fields.join(" ") : fields;
}
private attachmentExtractor(c: CipherView, joined: boolean) {
if (!c.hasAttachments) {
return null;
}
let attachments: string[] = [];
c.attachments.forEach((a) => {
if (a != null && a.fileName != null) {
if (joined && a.fileName.indexOf(".") > -1) {
attachments.push(a.fileName.substr(0, a.fileName.lastIndexOf(".")));
} else {
attachments.push(a.fileName);
}
}
});
attachments = attachments.filter((f) => f.trim() !== "");
if (attachments.length === 0) {
return null;
}
return joined ? attachments.join(" ") : attachments;
}
private uriExtractor(c: CipherView) {
if (c.type !== CipherType.Login || c.login == null || !c.login.hasUris) {
return null;
}
const uris: string[] = [];
c.login.uris.forEach((u) => {
if (u.uri == null || u.uri === "") {
return;
}
if (u.hostname != null) {
uris.push(u.hostname);
return;
}
let uri = u.uri;
if (u.match !== UriMatchType.RegularExpression) {
const protocolIndex = uri.indexOf("://");
if (protocolIndex > -1) {
uri = uri.substr(protocolIndex + 3);
}
const queryIndex = uri.search(/\?|&|#/);
if (queryIndex > -1) {
uri = uri.substring(0, queryIndex);
}
}
uris.push(uri);
});
return uris.length > 0 ? uris : null;
}
}

View File

@@ -0,0 +1,56 @@
import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service";
import { StateService } from "../abstractions/state.service";
const Keys = {
settingsPrefix: "settings_",
equivalentDomains: "equivalentDomains",
};
export class SettingsService implements SettingsServiceAbstraction {
constructor(private stateService: StateService) {}
async clearCache(): Promise<void> {
await this.stateService.setSettings(null);
}
getEquivalentDomains(): Promise<any> {
return this.getSettingsKey(Keys.equivalentDomains);
}
async setEquivalentDomains(equivalentDomains: string[][]): Promise<void> {
await this.setSettingsKey(Keys.equivalentDomains, equivalentDomains);
}
async clear(userId?: string): Promise<void> {
await this.stateService.setSettings(null, { userId: userId });
}
// Helpers
private async getSettings(): Promise<any> {
const settings = await this.stateService.getSettings();
if (settings == null) {
// eslint-disable-next-line
const userId = await this.stateService.getUserId();
}
return settings;
}
private async getSettingsKey(key: string): Promise<any> {
const settings = await this.getSettings();
if (settings != null && settings[key]) {
return settings[key];
}
return null;
}
private async setSettingsKey(key: string, value: any): Promise<void> {
let settings = await this.getSettings();
if (!settings) {
settings = {};
}
settings[key] = value;
await this.stateService.setSettings(settings);
}
}

View File

@@ -2172,11 +2172,11 @@ export class StateService<
}
const account = options?.useSecureStorage
? ((await this.secureStorageService.get<TAccount>(options.userId, options)) ??
? (await this.secureStorageService.get<TAccount>(options.userId, options)) ??
(await this.storageService.get<TAccount>(
options.userId,
this.reconcileOptions(options, { htmlStorageLocation: HtmlStorageLocation.Local }),
)))
))
: await this.storageService.get<TAccount>(options.userId, options);
if (this.useAccountCache) {

View File

@@ -0,0 +1,174 @@
import { ApiService } from "../abstractions/api.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 { OrganizationService } from "../abstractions/organization.service";
import { SettingsService } from "../abstractions/settings.service";
import { StateService } from "../abstractions/state.service";
import { SyncService as SyncServiceAbstraction } from "../abstractions/sync.service";
import { sequentialize } from "../misc/sequentialize";
import { OrganizationData } from "../models/data/organizationData";
import { DomainsResponse } from "../models/response/domainsResponse";
import { ProfileResponse } from "../models/response/profileResponse";
export class SyncService implements SyncServiceAbstraction {
syncInProgress = false;
constructor(
private apiService: ApiService,
private settingsService: SettingsService,
private cryptoService: CryptoService,
private messagingService: MessagingService,
private logService: LogService,
private keyConnectorService: KeyConnectorService,
private stateService: StateService,
private organizationService: OrganizationService,
private logoutCallback: (expired: boolean) => Promise<void>,
) {}
async getLastSync(): Promise<Date> {
if ((await this.stateService.getUserId()) == null) {
return null;
}
const lastSync = await this.stateService.getLastSync();
if (lastSync) {
return new Date(lastSync);
}
return null;
}
async setLastSync(date: Date, userId?: string): Promise<any> {
await this.stateService.setLastSync(date.toJSON(), { userId: userId });
}
@sequentialize(() => "fullSync")
async fullSync(forceSync: boolean, allowThrowOnError = false): Promise<boolean> {
this.syncStarted();
const isAuthenticated = await this.stateService.getIsAuthenticated();
if (!isAuthenticated) {
return this.syncCompleted(false);
}
const now = new Date();
let needsSync = false;
try {
needsSync = await this.needsSyncing(forceSync);
} catch (e) {
if (allowThrowOnError) {
throw e;
}
}
if (!needsSync) {
await this.setLastSync(now);
return this.syncCompleted(false);
}
try {
await this.apiService.refreshIdentityToken();
const response = await this.apiService.getSync();
await this.syncProfile(response.profile);
await this.syncSettings(response.domains);
await this.setLastSync(now);
return this.syncCompleted(true);
} catch (e) {
if (allowThrowOnError) {
throw e;
} else {
return this.syncCompleted(false);
}
}
}
// Helpers
private syncStarted() {
this.syncInProgress = true;
this.messagingService.send("syncStarted");
}
private syncCompleted(successfully: boolean): boolean {
this.syncInProgress = false;
this.messagingService.send("syncCompleted", { successfully: successfully });
return successfully;
}
private async needsSyncing(forceSync: boolean) {
if (forceSync) {
return true;
}
const lastSync = await this.getLastSync();
if (lastSync == null || lastSync.getTime() === 0) {
return true;
}
const response = await this.apiService.getAccountRevisionDate();
if (new Date(response) <= lastSync) {
return false;
}
return true;
}
private async syncProfile(response: ProfileResponse) {
const stamp = await this.stateService.getSecurityStamp();
if (stamp != null && stamp !== response.securityStamp) {
if (this.logoutCallback != null) {
await this.logoutCallback(true);
}
throw new Error("Stamp has changed");
}
await this.cryptoService.setEncKey(response.key);
await this.cryptoService.setEncPrivateKey(response.privateKey);
await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations);
await this.stateService.setSecurityStamp(response.securityStamp);
await this.stateService.setEmailVerified(response.emailVerified);
await this.stateService.setForcePasswordReset(response.forcePasswordReset);
await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector);
const organizations: { [id: string]: OrganizationData } = {};
response.organizations.forEach((o) => {
organizations[o.id] = new OrganizationData(o);
});
response.providerOrganizations.forEach((o) => {
if (organizations[o.id] == null) {
organizations[o.id] = new OrganizationData(o);
organizations[o.id].isProviderUser = true;
}
});
await this.organizationService.save(organizations);
if (await this.keyConnectorService.userNeedsMigration()) {
await this.keyConnectorService.setConvertAccountRequired(true);
this.messagingService.send("convertAccountToKeyConnector");
} else {
this.keyConnectorService.removeConvertAccountRequired();
}
}
private async syncSettings(response: DomainsResponse) {
let eqDomains: string[][] = [];
if (response != null && response.equivalentDomains != null) {
eqDomains = eqDomains.concat(response.equivalentDomains);
}
if (response != null && response.globalEquivalentDomains != null) {
response.globalEquivalentDomains.forEach((global) => {
if (global.domains.length > 0) {
eqDomains.push(global.domains);
}
});
}
return this.settingsService.setEquivalentDomains(eqDomains);
}
}

View File

@@ -0,0 +1,90 @@
import { MessagingService } from "../abstractions/messaging.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import { StateService } from "../abstractions/state.service";
import { SystemService as SystemServiceAbstraction } from "../abstractions/system.service";
import { Utils } from "../misc/utils";
export class SystemService implements SystemServiceAbstraction {
private reloadInterval: any = null;
private clearClipboardTimeout: any = null;
private clearClipboardTimeoutFunction: () => Promise<any> = null;
constructor(
private messagingService: MessagingService,
private platformUtilsService: PlatformUtilsService,
private reloadCallback: () => Promise<void> = null,
private stateService: StateService,
) {}
async startProcessReload(): Promise<void> {
if (
(await this.stateService.getDecryptedPinProtected()) != null ||
(await this.stateService.getBiometricLocked()) ||
this.reloadInterval != null
) {
return;
}
this.cancelProcessReload();
this.reloadInterval = setInterval(async () => {
let doRefresh = false;
const lastActive = await this.stateService.getLastActive();
if (lastActive != null) {
const diffSeconds = new Date().getTime() - lastActive;
// Don't refresh if they are still active in the window
doRefresh = diffSeconds >= 5000;
}
const biometricLockedFingerprintValidated =
(await this.stateService.getBiometricFingerprintValidated()) &&
(await this.stateService.getBiometricLocked());
if (doRefresh && !biometricLockedFingerprintValidated) {
clearInterval(this.reloadInterval);
this.reloadInterval = null;
this.messagingService.send("reloadProcess");
if (this.reloadCallback != null) {
await this.reloadCallback();
}
}
}, 10000);
}
cancelProcessReload(): void {
if (this.reloadInterval != null) {
clearInterval(this.reloadInterval);
this.reloadInterval = null;
}
}
async clearClipboard(clipboardValue: string, timeoutMs: number = null): Promise<void> {
if (this.clearClipboardTimeout != null) {
clearTimeout(this.clearClipboardTimeout);
this.clearClipboardTimeout = null;
}
if (Utils.isNullOrWhitespace(clipboardValue)) {
return;
}
await this.stateService.getClearClipboard().then((clearSeconds) => {
if (clearSeconds == null) {
return;
}
if (timeoutMs == null) {
timeoutMs = clearSeconds * 1000;
}
this.clearClipboardTimeoutFunction = async () => {
const clipboardValueNow = await this.platformUtilsService.readFromClipboard();
if (clipboardValue === clipboardValueNow) {
this.platformUtilsService.copyToClipboard("", { clearing: true });
}
};
this.clearClipboardTimeout = setTimeout(async () => {
await this.clearPendingClipboard();
}, timeoutMs);
});
}
async clearPendingClipboard() {
if (this.clearClipboardTimeoutFunction != null) {
await this.clearClipboardTimeoutFunction();
this.clearClipboardTimeoutFunction = null;
}
}
}

View File

@@ -0,0 +1,186 @@
import { I18nService } from "../abstractions/i18n.service";
import { PlatformUtilsService } from "../abstractions/platformUtils.service";
import {
TwoFactorProviderDetails,
TwoFactorService as TwoFactorServiceAbstraction,
} from "../abstractions/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;
}
}

View File

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

View File

@@ -0,0 +1,71 @@
import { CryptoFunctionService } from "@/jslib/common/src/abstractions/cryptoFunction.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { StateService } from "@/jslib/common/src/abstractions/state.service";
import { KeySuffixOptions } from "@/jslib/common/src/enums/keySuffixOptions";
import { SymmetricCryptoKey } from "@/jslib/common/src/models/domain/symmetricCryptoKey";
import { CryptoService } from "@/jslib/common/src/services/crypto.service";
export class ElectronCryptoService extends CryptoService {
constructor(
cryptoFunctionService: CryptoFunctionService,
platformUtilService: PlatformUtilsService,
logService: LogService,
stateService: StateService,
) {
super(cryptoFunctionService, platformUtilService, logService, stateService);
}
async hasKeyStored(keySuffix: KeySuffixOptions): Promise<boolean> {
await this.upgradeSecurelyStoredKey();
return super.hasKeyStored(keySuffix);
}
protected async storeKey(key: SymmetricCryptoKey, userId?: string) {
if (await this.shouldStoreKey(KeySuffixOptions.Auto, userId)) {
await this.stateService.setCryptoMasterKeyAuto(key.keyB64, { userId: userId });
} else {
this.clearStoredKey(KeySuffixOptions.Auto);
}
if (await this.shouldStoreKey(KeySuffixOptions.Biometric, userId)) {
await this.stateService.setCryptoMasterKeyBiometric(key.keyB64, { userId: userId });
} else {
this.clearStoredKey(KeySuffixOptions.Biometric);
}
}
protected async retrieveKeyFromStorage(keySuffix: KeySuffixOptions, userId?: string) {
await this.upgradeSecurelyStoredKey();
return super.retrieveKeyFromStorage(keySuffix, userId);
}
/**
* @deprecated 4 Jun 2021 This is temporary upgrade method to move from a single shared stored key to
* multiple, unique stored keys for each use, e.g. never logout vs. biometric authentication.
*/
private async upgradeSecurelyStoredKey() {
// attempt key upgrade, but if we fail just delete it. Keys will be stored property upon unlock anyway.
const key = await this.stateService.getCryptoMasterKeyB64();
if (key == null) {
return;
}
try {
if (await this.shouldStoreKey(KeySuffixOptions.Auto)) {
await this.stateService.setCryptoMasterKeyAuto(key);
}
if (await this.shouldStoreKey(KeySuffixOptions.Biometric)) {
await this.stateService.setCryptoMasterKeyBiometric(key);
}
} catch (e) {
this.logService.error(
`Encountered error while upgrading obsolete Bitwarden secure storage item:`,
);
this.logService.error(e);
}
await this.stateService.setCryptoMasterKeyB64(null);
}
}

View File

@@ -15,7 +15,7 @@ export class ElectronLogService extends BaseLogService {
super(isDev(), filter);
}
init() {
init () {
if (log.transports == null) {
return;
}

View File

@@ -14,7 +14,7 @@ export class WindowMain {
win: BrowserWindow;
isQuitting = false;
private windowStateChangeTimer: NodeJS.Timeout;
private windowStateChangeTimer: NodeJS.Timer;
private windowStates: { [key: string]: any } = {};
private enableAlwaysOnTop = false;

View File

@@ -0,0 +1,608 @@
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 { 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 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 kdf = await this.stateService.getKdfType();
const kdfIterations = await this.stateService.getKdfIterations();
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];
}
}

View File

@@ -1,12 +1,13 @@
import { AuthService } from "@/jslib/common/src/abstractions/auth.service";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { Response } from "@/jslib/node/src/cli/models/response";
import { MessageResponse } from "@/jslib/node/src/cli/models/response/messageResponse";
import { AuthService } from "../abstractions/auth.service";
import { Response } from "../models/response";
import { MessageResponse } from "../models/response/messageResponse";
export class LogoutCommand {
constructor(
private authService: AuthService,
private i18nService: I18nService,
private logoutCallback: () => Promise<void>,
) {}

View File

@@ -1,8 +1,13 @@
import * as child_process from "child_process";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { ClientType } from "@/jslib/common/src/enums/clientType";
import { DeviceType } from "@/jslib/common/src/enums/deviceType";
import { ThemeType } from "@/jslib/common/src/enums/themeType";
// eslint-disable-next-line
const open = require("open");
export class CliPlatformUtilsService implements PlatformUtilsService {
clientType: ClientType;
@@ -75,8 +80,12 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
return Promise.resolve(false);
}
launchUri(_uri: string, _options?: any): void {
throw new Error("Not implemented.");
launchUri(uri: string, options?: any): void {
if (process.platform === "linux") {
child_process.spawnSync("xdg-open", [uri]);
} else {
open(uri);
}
}
saveFile(win: Window, blobData: any, blobOptions: any, fileName: string): void {

2600
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
"name": "@bitwarden/directory-connector",
"productName": "Bitwarden Directory Connector",
"description": "Sync your user directory to your Bitwarden organization.",
"version": "2024.9.0",
"version": "2024.7.0",
"keywords": [
"bitwarden",
"password",
@@ -70,76 +70,75 @@
"test:types": "npx tsc --noEmit"
},
"devDependencies": {
"@angular-devkit/build-angular": "16.2.12",
"@angular-eslint/eslint-plugin-template": "17.2.0",
"@angular-eslint/template-parser": "17.2.0",
"@angular/compiler-cli": "16.2.12",
"@electron/notarize": "2.2.1",
"@electron/rebuild": "3.6.0",
"@fluffy-spoon/substitute": "1.208.0",
"@microsoft/microsoft-graph-types": "2.40.0",
"@ngtools/webpack": "16.2.12",
"@types/inquirer": "8.2.10",
"@types/inquirer": "8.2.6",
"@types/jest": "29.5.11",
"@types/ldapjs": "2.2.5",
"@types/lowdb": "1.0.15",
"@types/node": "18.19.50",
"@types/node-fetch": "2.6.11",
"@types/lunr": "2.3.7",
"@types/node": "18.17.12",
"@types/node-fetch": "2.6.10",
"@types/node-forge": "1.3.11",
"@types/proper-lockfile": "4.1.4",
"@types/tldjs": "2.3.4",
"@types/zxcvbn": "4.4.4",
"@typescript-eslint/eslint-plugin": "5.62.0",
"@typescript-eslint/parser": "5.62.0",
"clean-webpack-plugin": "4.0.0",
"concurrently": "9.0.1",
"concurrently": "8.2.2",
"copy-webpack-plugin": "12.0.2",
"cross-env": "7.0.3",
"css-loader": "6.11.0",
"dotenv": "16.4.5",
"electron": "28.3.3",
"css-loader": "6.9.0",
"dotenv": "16.4.1",
"electron": "28.2.0",
"electron-builder": "24.9.1",
"electron-log": "5.2.0",
"electron-log": "5.0.1",
"@electron/notarize": "2.2.1",
"@electron/rebuild": "3.6.0",
"electron-reload": "2.0.0-alpha.1",
"electron-store": "8.2.0",
"electron-updater": "6.3.0",
"electron-store": "8.1.0",
"electron-updater": "6.1.7",
"eslint": "8.56.0",
"eslint-config-prettier": "9.1.0",
"eslint-import-resolver-typescript": "3.6.3",
"eslint-plugin-import": "2.30.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-rxjs": "5.0.3",
"eslint-plugin-rxjs-angular": "2.0.1",
"form-data": "4.0.0",
"html-loader": "5.1.0",
"html-loader": "5.0.0",
"html-webpack-plugin": "5.6.0",
"husky": "9.0.10",
"jest": "29.7.0",
"jest-junit": "16.0.0",
"jest-mock-extended": "3.0.7",
"jest-preset-angular": "13.1.6",
"lint-staged": "15.2.10",
"mini-css-extract-plugin": "2.9.1",
"jest-preset-angular": "13.1.1",
"lint-staged": "15.2.0",
"mini-css-extract-plugin": "2.7.7",
"node-forge": "1.3.1",
"node-loader": "2.0.0",
"pkg": "5.8.1",
"prettier": "3.3.3",
"rimraf": "5.0.10",
"prettier": "3.2.2",
"rimraf": "5.0.5",
"rxjs": "7.8.1",
"sass": "1.78.0",
"sass-loader": "16.0.1",
"ts-jest": "29.2.5",
"sass": "1.69.7",
"sass-loader": "14.0.0",
"ts-jest": "29.1.1",
"ts-loader": "9.5.1",
"tsconfig-paths-webpack-plugin": "4.1.0",
"type-fest": "3.13.1",
"typescript": "4.9.5",
"typescript-transform-paths": "3.5.1",
"webpack": "5.94.0",
"typescript-transform-paths": "3.4.6",
"webpack": "5.89.0",
"webpack-cli": "5.1.4",
"webpack-merge": "6.0.1",
"webpack-merge": "5.10.0",
"webpack-node-externals": "3.0.0",
"zone.js": "0.13.1"
},
"dependencies": {
"@angular/animations": "16.2.12",
"@angular/cdk": "16.2.14",
"@angular/cdk": "16.2.12",
"@angular/common": "16.2.12",
"@angular/compiler": "16.2.12",
"@angular/core": "16.2.12",
@@ -148,11 +147,13 @@
"@angular/platform-browser-dynamic": "16.2.12",
"@angular/router": "16.2.12",
"@microsoft/microsoft-graph-client": "3.0.7",
"@microsoft/signalr": "7.0.10",
"@microsoft/signalr-protocol-msgpack": "7.0.10",
"big-integer": "1.6.52",
"bootstrap": "5.0.0",
"bootstrap": "4.6.2",
"browser-hrtime": "1.1.8",
"chalk": "4.1.2",
"commander": "12.1.0",
"commander": "12.0.0",
"core-js": "3.35.0",
"form-data": "4.0.0",
"google-auth-library": "7.14.1",
@@ -160,18 +161,21 @@
"https-proxy-agent": "7.0.4",
"inquirer": "8.2.6",
"keytar": "7.9.0",
"ldapts": "7.2.0",
"ldapjs": "2.3.3",
"lowdb": "1.0.0",
"lunr": "2.3.9",
"ngx-toastr": "16.2.0",
"node-fetch": "2.7.0",
"open": "8.4.2",
"proper-lockfile": "4.1.2",
"rxjs": "7.8.1",
"tldjs": "2.3.1",
"zone.js": "0.13.1"
"zone.js": "0.13.1",
"zxcvbn": "4.4.2"
},
"engines": {
"node": "~18",
"npm": "~10"
"npm": "~10.4.0"
},
"lint-staged": {
"./!(jslib)**": "prettier --ignore-unknown --write",

View File

@@ -7,30 +7,15 @@ exports.default = async function notarizing(context) {
if (electronPlatformName !== "darwin") {
return;
}
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
const appName = context.packager.appInfo.productFilename;
if (process.env.APP_STORE_CONNECT_TEAM_ISSUER) {
const appleApiIssuer = process.env.APP_STORE_CONNECT_TEAM_ISSUER;
const appleApiKey = process.env.APP_STORE_CONNECT_AUTH_KEY_PATH;
const appleApiKeyId = process.env.APP_STORE_CONNECT_AUTH_KEY;
return await notarize({
tool: "notarytool",
appBundleId: "com.bitwarden.directory-connector",
appPath: `${appOutDir}/${appName}.app`,
appleApiIssuer: appleApiIssuer,
appleApiKey: appleApiKey,
appleApiKeyId: appleApiKeyId,
});
} else {
const appleId = process.env.APPLE_ID_USERNAME || process.env.APPLEID;
const appleIdPassword = process.env.APPLE_ID_PASSWORD || `@keychain:AC_PASSWORD`;
return await notarize({
tool: "notarytool",
appBundleId: "com.bitwarden.directory-connector",
appPath: `${appOutDir}/${appName}.app`,
teamId: "LTZ2PFU5D6",
appleId: appleId,
appleIdPassword: appleIdPassword,
});
}
return await notarize({
tool: "notarytool",
appBundleId: "com.bitwarden.directory-connector",
appPath: `${appOutDir}/${appName}.app`,
teamId: "LTZ2PFU5D6",
appleId: appleId,
appleIdPassword: appleIdPassword,
});
};

View File

@@ -1,4 +0,0 @@
export abstract class AuthService {
logIn: (credentials: { clientId: string; clientSecret: string }) => Promise<void>;
logOut: (callback: () => void) => void;
}

View File

@@ -2,33 +2,41 @@
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="row justify-content-center">
<div class="col-md-8 col-lg-6">
<p class="text-center fw-bold">{{ "welcome" | i18n }}</p>
<p class="text-center font-weight-bold">{{ "welcome" | i18n }}</p>
<p class="text-center">{{ "logInDesc" | i18n }}</p>
<div class="card">
<h5 class="card-header">{{ "logIn" | i18n }}</h5>
<div class="card-body">
<div class="mb-3">
<label for="client_id" class="form-label">{{ "clientId" | i18n }}</label>
<div class="form-group">
<label for="client_id">{{ "clientId" | i18n }}</label>
<input id="client_id" name="ClientId" [(ngModel)]="clientId" class="form-control" />
</div>
<div class="mb-3">
<label for="client_secret" class="form-label">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showSecret ? 'text' : 'password' }}"
id="client_secret"
name="ClientSecret"
[(ngModel)]="clientSecret"
class="form-control"
/>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleSecret()"
>
<i class="bwi" [ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"></i>
</button>
<div class="form-group">
<div class="row-main">
<label for="client_secret">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showSecret ? 'text' : 'password' }}"
id="client_secret"
name="ClientSecret"
[(ngModel)]="clientSecret"
class="form-control"
/>
<div class="input-group-append">
<button
type="button"
class="ml-1 btn btn-link"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="d-flex">
@@ -39,7 +47,7 @@
{{ "logIn" | i18n }}
</button>
</div>
<button type="button" class="btn btn-link ms-auto" (click)="settings()">
<button type="button" class="btn btn-link ml-auto" (click)="settings()">
{{ "settings" | i18n }}
</button>
</div>

View File

@@ -3,12 +3,13 @@ import { Router } from "@angular/router";
import { takeUntil } from "rxjs";
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 { LogService } from "@/jslib/common/src/abstractions/log.service";
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
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 { EnvironmentComponent } from "./environment.component";
@@ -80,10 +81,9 @@ export class ApiKeyComponent {
}
try {
this.formPromise = this.authService.logIn({
clientId: this.clientId,
clientSecret: this.clientSecret,
});
this.formPromise = this.authService.logIn(
new ApiLogInCredentials(this.clientId, this.clientSecret),
);
await this.formPromise;
const organizationId = await this.stateService.getEntityId();
await this.stateService.setOrganizationId(organizationId);

View File

@@ -3,13 +3,15 @@
<form class="modal-content" (ngSubmit)="submit()">
<div class="modal-header">
<h3 class="modal-title">{{ "settings" | i18n }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
<button type="button" class="close" data-dismiss="modal" title="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<h4>{{ "selfHostedEnvironment" | i18n }}</h4>
<p>{{ "selfHostedEnvironmentFooter" | i18n }}</p>
<div class="mb-3">
<label for="baseUrl" class="form-label">{{ "baseUrl" | i18n }}</label>
<div class="form-group">
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="baseUrl"
type="text"
@@ -17,12 +19,14 @@
[(ngModel)]="baseUrl"
class="form-control"
/>
<div class="form-text">{{ "ex" | i18n }} https://bitwarden.company.com</div>
<small class="text-muted form-text"
>{{ "ex" | i18n }} https://bitwarden.company.com</small
>
</div>
<h4>{{ "customEnvironment" | i18n }}</h4>
<p>{{ "customEnvironmentFooter" | i18n }}</p>
<div class="mb-3">
<label for="webVaultUrl" class="form-label">{{ "webVaultUrl" | i18n }}</label>
<div class="form-group">
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
<input
id="webVaultUrl"
type="text"
@@ -31,12 +35,12 @@
class="form-control"
/>
</div>
<div class="mb-3">
<label for="apiUrl" class="form-label">{{ "apiUrl" | i18n }}</label>
<div class="form-group">
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" class="form-control" />
</div>
<div class="mb-3">
<label for="identityUrl" class="form-label">{{ "identityUrl" | i18n }}</label>
<div class="form-group">
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
<input
id="identityUrl"
type="text"

View File

@@ -10,6 +10,7 @@ import { DomSanitizer } from "@angular/platform-browser";
import { Router } from "@angular/router";
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 { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
@@ -17,7 +18,6 @@ import { MessagingService } from "@/jslib/common/src/abstractions/messaging.serv
import { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { AuthService } from "../abstractions/auth.service";
import { StateService } from "../abstractions/state.service";
import { SyncService } from "../services/sync.service";

View File

@@ -1,17 +0,0 @@
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");

View File

@@ -1,144 +0,0 @@
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;

View File

@@ -3,6 +3,7 @@ import { APP_INITIALIZER, NgModule } from "@angular/core";
import { JslibServicesModule } from "@/jslib/angular/src/services/jslib-services.module";
import { ApiService as ApiServiceAbstraction } from "@/jslib/common/src/abstractions/api.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 { CryptoService as CryptoServiceAbstraction } from "@/jslib/common/src/abstractions/crypto.service";
import { CryptoFunctionService as CryptoFunctionServiceAbstraction } from "@/jslib/common/src/abstractions/cryptoFunction.service";
@@ -14,6 +15,7 @@ import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@/jslib
import { StateMigrationService as StateMigrationServiceAbstraction } from "@/jslib/common/src/abstractions/stateMigration.service";
import { StorageService as StorageServiceAbstraction } from "@/jslib/common/src/abstractions/storage.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 { GlobalState } from "@/jslib/common/src/models/domain/globalState";
import { ContainerService } from "@/jslib/common/src/services/container.service";
@@ -25,23 +27,21 @@ import { ElectronRendererStorageService } from "@/jslib/electron/src/services/el
import { NodeApiService } from "@/jslib/node/src/services/nodeApi.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 { Account } from "../../models/account";
import { AuthService } from "../../services/auth.service";
import { I18nService } from "../../services/i18n.service";
import { NoopTwoFactorService } from "../../services/noop/noopTwoFactor.service";
import { StateService } from "../../services/state.service";
import { StateMigrationService } from "../../services/stateMigration.service";
import { SyncService } from "../../services/sync.service";
import { AuthGuardService } from "./auth-guard.service";
import { SafeInjectionToken, SECURE_STORAGE, WINDOW } from "./injection-tokens";
import { LaunchGuardService } from "./launch-guard.service";
import { SafeProvider, safeProvider } from "./safe-provider";
export function initFactory(
environmentService: EnvironmentServiceAbstraction,
i18nService: I18nServiceAbstraction,
i18nService: I18nService,
platformUtilsService: PlatformUtilsServiceAbstraction,
stateService: StateServiceAbstraction,
cryptoService: CryptoServiceAbstraction,
@@ -49,7 +49,7 @@ export function initFactory(
return async () => {
await stateService.init();
await environmentService.setUrlsFromStorage();
await (i18nService as I18nService).init();
await i18nService.init();
const htmlEl = window.document.documentElement;
htmlEl.classList.add("os_" + platformUtilsService.getDeviceString());
htmlEl.classList.add("locale_" + i18nService.translationLocale);
@@ -77,8 +77,8 @@ export function initFactory(
imports: [JslibServicesModule],
declarations: [],
providers: [
safeProvider({
provide: APP_INITIALIZER as SafeInjectionToken<() => void>,
{
provide: APP_INITIALIZER,
useFactory: initFactory,
deps: [
EnvironmentServiceAbstraction,
@@ -88,29 +88,21 @@ export function initFactory(
CryptoServiceAbstraction,
],
multi: true,
}),
safeProvider({ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] }),
safeProvider({
},
{ provide: LogServiceAbstraction, useClass: ElectronLogService, deps: [] },
{
provide: I18nServiceAbstraction,
useFactory: (window: Window) => new I18nService(window.navigator.language, "./locales"),
deps: [WINDOW],
}),
safeProvider({
deps: ["WINDOW"],
},
{
provide: MessagingServiceAbstraction,
useClass: ElectronRendererMessagingService,
deps: [BroadcasterServiceAbstraction],
}),
safeProvider({
provide: StorageServiceAbstraction,
useClass: ElectronRendererStorageService,
deps: [],
}),
safeProvider({
provide: SECURE_STORAGE,
useClass: ElectronRendererSecureStorageService,
deps: [],
}),
safeProvider({
},
{ provide: StorageServiceAbstraction, useClass: ElectronRendererStorageService },
{ provide: "SECURE_STORAGE", useClass: ElectronRendererSecureStorageService },
{
provide: PlatformUtilsServiceAbstraction,
useFactory: (
i18nService: I18nServiceAbstraction,
@@ -118,13 +110,9 @@ export function initFactory(
stateService: StateServiceAbstraction,
) => new ElectronPlatformUtilsService(i18nService, messagingService, false, stateService),
deps: [I18nServiceAbstraction, MessagingServiceAbstraction, StateServiceAbstraction],
}),
safeProvider({
provide: CryptoFunctionServiceAbstraction,
useClass: NodeCryptoFunctionService,
deps: [],
}),
safeProvider({
},
{ provide: CryptoFunctionServiceAbstraction, useClass: NodeCryptoFunctionService, deps: [] },
{
provide: ApiServiceAbstraction,
useFactory: (
tokenService: TokenServiceAbstraction,
@@ -152,19 +140,25 @@ export function initFactory(
MessagingServiceAbstraction,
AppIdServiceAbstraction,
],
}),
safeProvider({
},
{
provide: AuthServiceAbstraction,
useClass: AuthService,
deps: [
CryptoServiceAbstraction,
ApiServiceAbstraction,
TokenServiceAbstraction,
AppIdServiceAbstraction,
PlatformUtilsServiceAbstraction,
MessagingServiceAbstraction,
LogServiceAbstraction,
EnvironmentServiceAbstraction,
StateServiceAbstraction,
TwoFactorServiceAbstraction,
I18nServiceAbstraction,
],
}),
safeProvider({
},
{
provide: SyncService,
useClass: SyncService,
deps: [
@@ -176,10 +170,10 @@ export function initFactory(
EnvironmentServiceAbstraction,
StateServiceAbstraction,
],
}),
safeProvider(AuthGuardService),
safeProvider(LaunchGuardService),
safeProvider({
},
AuthGuardService,
LaunchGuardService,
{
provide: StateMigrationServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -190,9 +184,9 @@ export function initFactory(
secureStorageService,
new StateFactory(GlobalState, Account),
),
deps: [StorageServiceAbstraction, SECURE_STORAGE],
}),
safeProvider({
deps: [StorageServiceAbstraction, "SECURE_STORAGE"],
},
{
provide: StateServiceAbstraction,
useFactory: (
storageService: StorageServiceAbstraction,
@@ -210,11 +204,15 @@ export function initFactory(
),
deps: [
StorageServiceAbstraction,
SECURE_STORAGE,
"SECURE_STORAGE",
LogServiceAbstraction,
StateMigrationServiceAbstraction,
],
}),
] satisfies SafeProvider[],
},
{
provide: TwoFactorServiceAbstraction,
useClass: NoopTwoFactorService,
},
],
})
export class ServicesModule {}

View File

@@ -105,11 +105,6 @@
<li *ngFor="let g of simGroups" title="{{ g.referenceId }}">
<i class="bwi bwi-li bwi-sitemap"></i>
{{ g.displayName }}
<ul class="small" *ngIf="g.users && g.users.length">
<li *ngFor="let u of g.users" title="{{ u.referenceId }}">
{{ u.displayName }}
</li>
</ul>
</li>
</ul>
<p *ngIf="!simGroups || !simGroups.length">{{ "noGroups" | i18n }}</p>

View File

@@ -1,19 +1,19 @@
<div class="row g-3">
<div class="row">
<div class="col-sm">
<div class="card mb-3">
<h3 class="card-header">{{ "directory" | i18n }}</h3>
<div class="card-body">
<div class="mb-3">
<label for="directory" class="form-label">{{ "type" | i18n }}</label>
<select class="form-select" id="directory" name="Directory" [(ngModel)]="directory">
<div class="form-group">
<label for="directory">{{ "type" | i18n }}</label>
<select class="form-control" id="directory" name="Directory" [(ngModel)]="directory">
<option *ngFor="let o of directoryOptions" [ngValue]="o.value">
{{ o.name }}
</option>
</select>
</div>
<div [hidden]="directory != directoryType.Ldap">
<div class="mb-3">
<label for="hostname" class="form-label">{{ "serverHostname" | i18n }}</label>
<div class="form-group">
<label for="hostname">{{ "serverHostname" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -21,15 +21,15 @@
name="Hostname"
[(ngModel)]="ldap.hostname"
/>
<div class="form-text">{{ "ex" | i18n }} ad.company.com</div>
<small class="text-muted form-text">{{ "ex" | i18n }} ad.company.com</small>
</div>
<div class="mb-3">
<label for="port" class="form-label">{{ "port" | i18n }}</label>
<div class="form-group">
<label for="port">{{ "port" | i18n }}</label>
<input type="text" class="form-control" id="port" name="Port" [(ngModel)]="ldap.port" />
<div class="form-text">{{ "ex" | i18n }} 389</div>
<small class="text-muted form-text">{{ "ex" | i18n }} 389</small>
</div>
<div class="mb-3">
<label for="rootPath" class="form-label">{{ "rootPath" | i18n }}</label>
<div class="form-group">
<label for="rootPath">{{ "rootPath" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -37,9 +37,9 @@
name="RootPath"
[(ngModel)]="ldap.rootPath"
/>
<div class="form-text">{{ "ex" | i18n }} dc=company,dc=com</div>
<small class="text-muted form-text">{{ "ex" | i18n }} dc=company,dc=com</small>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -51,7 +51,7 @@
<label class="form-check-label" for="ad">{{ "ldapAd" | i18n }}</label>
</div>
</div>
<div class="mb-3" *ngIf="!ldap.ad">
<div class="form-group" *ngIf="!ldap.ad">
<div class="form-check">
<input
class="form-check-input"
@@ -65,7 +65,7 @@
}}</label>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -79,38 +79,38 @@
}}</label>
</div>
</div>
<div class="ms-4" *ngIf="ldap.ssl">
<div class="mb-3">
<div class="form-check">
<div class="ml-4" *ngIf="ldap.ssl">
<div class="form-group">
<div class="form-radio">
<input
class="form-check-input"
class="form-radio-input"
type="radio"
[value]="false"
id="ssl"
[(ngModel)]="ldap.startTls"
name="SSL"
/>
<label class="form-check-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
<label class="form-radio-label" for="ssl">{{ "ldapSsl" | i18n }}</label>
</div>
<div class="form-check">
<div class="form-radio">
<input
class="form-check-input"
class="form-radio-input"
type="radio"
[value]="true"
id="startTls"
[(ngModel)]="ldap.startTls"
name="StartTLS"
/>
<label class="form-check-label" for="startTls">{{ "ldapTls" | i18n }}</label>
<label class="form-radio-label" for="startTls">{{ "ldapTls" | i18n }}</label>
</div>
</div>
<div class="ms-4" *ngIf="ldap.startTls">
<div class="ml-4" *ngIf="ldap.startTls">
<p>{{ "ldapTlsUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="tlsCaPath" class="form-label">{{ "ldapTlsCa" | i18n }}</label>
<div class="form-group">
<label for="tlsCaPath">{{ "ldapTlsCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
class="form-control-file mb-2"
id="tlsCaPath_file"
(change)="setSslPath('tlsCaPath')"
/>
@@ -123,13 +123,13 @@
/>
</div>
</div>
<div class="ms-4" *ngIf="!ldap.startTls">
<div class="ml-4" *ngIf="!ldap.startTls">
<p>{{ "ldapSslUntrustedDesc" | i18n }}</p>
<div class="mb-3">
<label for="sslCertPath" class="form-label">{{ "ldapSslCert" | i18n }}</label>
<div class="form-group">
<label for="sslCertPath">{{ "ldapSslCert" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
class="form-control-file mb-2"
id="sslCertPath_file"
(change)="setSslPath('sslCertPath')"
/>
@@ -141,11 +141,11 @@
[(ngModel)]="ldap.sslCertPath"
/>
</div>
<div class="mb-3">
<label for="sslKeyPath" class="form-label">{{ "ldapSslKey" | i18n }}</label>
<div class="form-group">
<label for="sslKeyPath">{{ "ldapSslKey" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
class="form-control-file mb-2"
id="sslKeyPath_file"
(change)="setSslPath('sslKeyPath')"
/>
@@ -157,11 +157,11 @@
[(ngModel)]="ldap.sslKeyPath"
/>
</div>
<div class="mb-3">
<label for="sslCaPath" class="form-label">{{ "ldapSslCa" | i18n }}</label>
<div class="form-group">
<label for="sslCaPath">{{ "ldapSslCa" | i18n }}</label>
<input
type="file"
class="form-control mb-2"
class="form-control-file mb-2"
id="sslCaPath_file"
(change)="setSslPath('sslCaPath')"
/>
@@ -174,7 +174,7 @@
/>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -189,7 +189,7 @@
</div>
</div>
</div>
<div class="mb-3" [hidden]="true">
<div class="form-group" [hidden]="true">
<div class="form-check">
<input
class="form-check-input"
@@ -202,8 +202,8 @@
</div>
</div>
<div [hidden]="ldap.currentUser">
<div class="mb-3">
<label for="username" class="form-label">{{ "username" | i18n }}</label>
<div class="form-group">
<label for="username">{{ "username" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -211,13 +211,15 @@
name="Username"
[(ngModel)]="ldap.username"
/>
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} company\admin</div>
<div class="form-text" *ngIf="!ldap.ad">
{{ "ex" | i18n }} cn=admin,dc=company,dc=com
</div>
<small class="text-muted form-text" *ngIf="ldap.ad"
>{{ "ex" | i18n }} company\admin</small
>
<small class="text-muted form-text" *ngIf="!ldap.ad"
>{{ "ex" | i18n }} cn=admin,dc=company,dc=com</small
>
</div>
<div class="mb-3">
<label for="password" class="form-label">{{ "password" | i18n }}</label>
<div class="form-group">
<label for="password">{{ "password" | i18n }}</label>
<div class="input-group">
<input
type="{{ showLdapPassword ? 'text' : 'password' }}"
@@ -226,29 +228,29 @@
name="Password"
[(ngModel)]="ldap.password"
/>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleLdapPassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showLdapPassword ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleLdapPassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showLdapPassword ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.AzureActiveDirectory">
<div class="mb-3">
<label for="identityAuthority" class="form-label">{{
"identityAuthority" | i18n
}}</label>
<div class="form-group">
<label for="identityAuthority">{{ "identityAuthority" | i18n }}</label>
<select
class="form-select"
class="form-control"
id="identityAuthority"
name="IdentityAuthority"
[(ngModel)]="azure.identityAuthority"
@@ -257,8 +259,8 @@
<option value="login.microsoftonline.us">Azure AD Government</option>
</select>
</div>
<div class="mb-3">
<label for="tenant" class="form-label">{{ "tenant" | i18n }}</label>
<div class="form-group">
<label for="tenant">{{ "tenant" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -266,10 +268,10 @@
name="Tenant"
[(ngModel)]="azure.tenant"
/>
<div class="form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</div>
<small class="text-muted form-text">{{ "ex" | i18n }} companyad.onmicrosoft.com</small>
</div>
<div class="mb-3">
<label for="applicationId" class="form-label">{{ "applicationId" | i18n }}</label>
<div class="form-group">
<label for="applicationId">{{ "applicationId" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -278,8 +280,8 @@
[(ngModel)]="azure.applicationId"
/>
</div>
<div class="mb-3">
<label for="secretKey" class="form-label">{{ "secretKey" | i18n }}</label>
<div class="form-group">
<label for="secretKey">{{ "secretKey" | i18n }}</label>
<div class="input-group">
<input
type="{{ showAzureKey ? 'text' : 'password' }}"
@@ -288,24 +290,26 @@
name="SecretKey"
[(ngModel)]="azure.key"
/>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleAzureKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleAzureKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showAzureKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.Okta">
<div class="mb-3">
<label for="orgUrl" class="form-label">{{ "organizationUrl" | i18n }}</label>
<div class="form-group">
<label for="orgUrl">{{ "organizationUrl" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -313,10 +317,12 @@
name="OrgUrl"
[(ngModel)]="okta.orgUrl"
/>
<div class="form-text">{{ "ex" | i18n }} https://mycompany.okta.com/</div>
<small class="text-muted form-text"
>{{ "ex" | i18n }} https://mycompany.okta.com/</small
>
</div>
<div class="mb-3">
<label for="oktaToken" class="form-label">{{ "token" | i18n }}</label>
<div class="form-group">
<label for="oktaToken">{{ "token" | i18n }}</label>
<div class="input-group">
<input
type="{{ showOktaKey ? 'text' : 'password' }}"
@@ -325,24 +331,26 @@
name="OktaToken"
[(ngModel)]="okta.token"
/>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOktaKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOktaKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOktaKey()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOktaKey ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.OneLogin">
<div class="mb-3">
<label for="oneLoginClientId" class="form-label">{{ "clientId" | i18n }}</label>
<div class="form-group">
<label for="oneLoginClientId">{{ "clientId" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -351,8 +359,8 @@
[(ngModel)]="oneLogin.clientId"
/>
</div>
<div class="mb-3">
<label for="oneLoginClientSecret" class="form-label">{{ "clientSecret" | i18n }}</label>
<div class="form-group">
<label for="oneLoginClientSecret">{{ "clientSecret" | i18n }}</label>
<div class="input-group">
<input
type="{{ showOneLoginSecret ? 'text' : 'password' }}"
@@ -361,24 +369,26 @@
name="OneLoginClientSecret"
[(ngModel)]="oneLogin.clientSecret"
/>
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOneLoginSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOneLoginSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
<div class="input-group-append">
<button
type="button"
class="btn btn-outline-secondary"
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
(click)="toggleOneLoginSecret()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="showOneLoginSecret ? 'bwi-eye-slash' : 'bwi-eye'"
></i>
</button>
</div>
</div>
</div>
<div class="mb-3">
<label for="oneLoginRegion" class="form-label">{{ "region" | i18n }}</label>
<div class="form-group">
<label for="oneLoginRegion">{{ "region" | i18n }}</label>
<select
class="form-select"
class="form-control"
id="oneLoginRegion"
name="OneLoginRegion"
[(ngModel)]="oneLogin.region"
@@ -389,8 +399,8 @@
</div>
</div>
<div [hidden]="directory != directoryType.GSuite">
<div class="mb-3">
<label for="domain" class="form-label">{{ "domain" | i18n }}</label>
<div class="form-group">
<label for="domain">{{ "domain" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -398,10 +408,10 @@
name="Domain"
[(ngModel)]="gsuite.domain"
/>
<div class="form-text">{{ "ex" | i18n }} company.com</div>
<small class="text-muted form-text">{{ "ex" | i18n }} company.com</small>
</div>
<div class="mb-3">
<label for="adminUser" class="form-label">{{ "adminUser" | i18n }}</label>
<div class="form-group">
<label for="adminUser">{{ "adminUser" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -409,10 +419,10 @@
name="AdminUser"
[(ngModel)]="gsuite.adminUser"
/>
<div class="form-text">{{ "ex" | i18n }} admin&#64;company.com</div>
<small class="text-muted form-text">{{ "ex" | i18n }} admin@company.com</small>
</div>
<div class="mb-3">
<label for="customerId" class="form-label">{{ "customerId" | i18n }}</label>
<div class="form-group">
<label for="customerId">{{ "customerId" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -420,21 +430,21 @@
name="CustomerId"
[(ngModel)]="gsuite.customer"
/>
<div class="form-text">{{ "ex" | i18n }} 39204722352</div>
<small class="text-muted form-text">{{ "ex" | i18n }} 39204722352</small>
</div>
<div class="mb-3">
<label for="keyFile" class="form-label">{{ "jsonKeyFile" | i18n }}</label>
<div class="form-group">
<label for="keyFile">{{ "jsonKeyFile" | i18n }}</label>
<input
type="file"
class="form-control"
class="form-control-file"
id="keyFile"
accept=".json"
(change)="parseKeyFile()"
/>
<div class="form-text">{{ "ex" | i18n }} My Project-jksd3jd223.json</div>
<small class="text-muted form-text">{{ "ex" | i18n }} My Project-jksd3jd223.json</small>
</div>
<div class="mb-3" [hidden]="!gsuite.clientEmail">
<label for="clientEmail" class="form-label">{{ "clientEmail" | i18n }}</label>
<div class="form-group" [hidden]="!gsuite.clientEmail">
<label for="clientEmail">{{ "clientEmail" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -443,8 +453,8 @@
[(ngModel)]="gsuite.clientEmail"
/>
</div>
<div class="mb-3" [hidden]="!gsuite.privateKey">
<label for="privateKey" class="form-label">{{ "privateKey" | i18n }}</label>
<div class="form-group" [hidden]="!gsuite.privateKey">
<label for="privateKey">{{ "privateKey" | i18n }}</label>
<textarea
class="form-control text-monospace"
id="privateKey"
@@ -461,8 +471,8 @@
<div class="card">
<h3 class="card-header">{{ "sync" | i18n }}</h3>
<div class="card-body">
<div class="mb-3">
<label for="interval" class="form-label">{{ "interval" | i18n }}</label>
<div class="form-group">
<label for="interval">{{ "interval" | i18n }}</label>
<input
type="number"
min="5"
@@ -471,9 +481,9 @@
name="Interval"
[(ngModel)]="sync.interval"
/>
<div class="form-text">{{ "intervalMin" | i18n }}</div>
<small class="text-muted form-text">{{ "intervalMin" | i18n }}</small>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -487,7 +497,7 @@
}}</label>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -501,7 +511,7 @@
}}</label>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -515,8 +525,8 @@
</div>
<div [hidden]="directory != directoryType.Ldap">
<div [hidden]="ldap.ad">
<div class="mb-3">
<label for="memberAttribute" class="form-label">{{ "memberAttribute" | i18n }}</label>
<div class="form-group">
<label for="memberAttribute">{{ "memberAttribute" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -524,12 +534,10 @@
name="MemberAttribute"
[(ngModel)]="sync.memberAttribute"
/>
<div class="form-text">{{ "ex" | i18n }} uniqueMember</div>
<small class="text-muted form-text">{{ "ex" | i18n }} uniqueMember</small>
</div>
<div class="mb-3">
<label for="creationDateAttribute" class="form-label">{{
"creationDateAttribute" | i18n
}}</label>
<div class="form-group">
<label for="creationDateAttribute">{{ "creationDateAttribute" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -537,12 +545,10 @@
name="CreationDateAttribute"
[(ngModel)]="sync.creationDateAttribute"
/>
<div class="form-text">{{ "ex" | i18n }} whenCreated</div>
<small class="text-muted form-text">{{ "ex" | i18n }} whenCreated</small>
</div>
<div class="mb-3">
<label for="revisionDateAttribute" class="form-label">{{
"revisionDateAttribute" | i18n
}}</label>
<div class="form-group">
<label for="revisionDateAttribute">{{ "revisionDateAttribute" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -550,12 +556,12 @@
name="RevisionDateAttribute"
[(ngModel)]="sync.revisionDateAttribute"
/>
<div class="form-text">{{ "ex" | i18n }} whenChanged</div>
<small class="text-muted form-text">{{ "ex" | i18n }} whenChanged</small>
</div>
</div>
</div>
<div [hidden]="directory != directoryType.Ldap && directory != directoryType.OneLogin">
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -570,10 +576,8 @@
</div>
</div>
<div [hidden]="!sync.useEmailPrefixSuffix">
<div class="mb-3" [hidden]="ldap.ad || directory != directoryType.Ldap">
<label for="emailPrefixAttribute" class="form-label">{{
"emailPrefixAttribute" | i18n
}}</label>
<div class="form-group" [hidden]="ldap.ad || directory != directoryType.Ldap">
<label for="emailPrefixAttribute">{{ "emailPrefixAttribute" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -581,10 +585,10 @@
name="EmailPrefixAttribute"
[(ngModel)]="sync.emailPrefixAttribute"
/>
<div class="form-text">{{ "ex" | i18n }} accountName</div>
<small class="text-muted form-text">{{ "ex" | i18n }} accountName</small>
</div>
<div class="mb-3">
<label for="emailSuffix" class="form-label">{{ "emailSuffix" | i18n }}</label>
<div class="form-group">
<label for="emailSuffix">{{ "emailSuffix" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -592,12 +596,12 @@
name="EmailSuffix"
[(ngModel)]="sync.emailSuffix"
/>
<div class="form-text">{{ "ex" | i18n }} &#64;company.com</div>
<small class="text-muted form-text">{{ "ex" | i18n }} @company.com</small>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -610,29 +614,31 @@
</div>
</div>
<div [hidden]="!sync.users">
<div class="mb-3">
<label for="userFilter" class="form-label">{{ "userFilter" | i18n }}</label>
<div class="form-group">
<label for="userFilter">{{ "userFilter" | i18n }}</label>
<textarea
class="form-control"
id="userFilter"
name="UserFilter"
[(ngModel)]="sync.userFilter"
></textarea>
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))
</div>
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory">
{{ "ex" | i18n }} exclude:joe&#64;company.com
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} exclude:joe&#64;company.com | profile.firstName eq "John"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} exclude:joe&#64;company.com | orgName=Engineering
</div>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap"
>{{ "ex" | i18n }} (&amp;(givenName=John)(|(l=Dallas)(l=Austin)))</small
>
<small
class="text-muted form-text"
*ngIf="directory === directoryType.AzureActiveDirectory"
>{{ "ex" | i18n }} exclude:joe@company.com</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
>{{ "ex" | i18n }} exclude:joe@company.com | profile.firstName eq "John"</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
>{{ "ex" | i18n }} exclude:joe@company.com | orgName=Engineering</small
>
</div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="userPath" class="form-label">{{ "userPath" | i18n }}</label>
<div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="userPath">{{ "userPath" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -640,11 +646,11 @@
name="UserPath"
[(ngModel)]="sync.userPath"
/>
<div class="form-text">{{ "ex" | i18n }} CN=Users</div>
<small class="text-muted form-text">{{ "ex" | i18n }} CN=Users</small>
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="mb-3">
<label for="userObjectClass" class="form-label">{{ "userObjectClass" | i18n }}</label>
<div class="form-group">
<label for="userObjectClass">{{ "userObjectClass" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -652,12 +658,10 @@
name="UserObjectClass"
[(ngModel)]="sync.userObjectClass"
/>
<div class="form-text">{{ "ex" | i18n }} inetOrgPerson</div>
<small class="text-muted form-text">{{ "ex" | i18n }} inetOrgPerson</small>
</div>
<div class="mb-3">
<label for="userEmailAttribute" class="form-label">{{
"userEmailAttribute" | i18n
}}</label>
<div class="form-group">
<label for="userEmailAttribute">{{ "userEmailAttribute" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -665,12 +669,12 @@
name="UserEmailAttribute"
[(ngModel)]="sync.userEmailAttribute"
/>
<div class="form-text">{{ "ex" | i18n }} mail</div>
<small class="text-muted form-text">{{ "ex" | i18n }} mail</small>
</div>
</div>
</div>
<div class="mb-3">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
@@ -685,8 +689,8 @@
</div>
</div>
<div [hidden]="!sync.groups">
<div class="mb-3">
<label for="groupFilter" class="form-label">{{
<div class="form-group">
<label for="groupFilter">{{
(directory !== directoryType.OneLogin ? "groupFilter" : "groupFilterOneLogin") | i18n
}}</label>
<textarea
@@ -695,21 +699,23 @@
name="GroupFilter"
[(ngModel)]="sync.groupFilter"
></textarea>
<div class="form-text" *ngIf="directory === directoryType.Ldap">
{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))
</div>
<div class="form-text" *ngIf="directory === directoryType.AzureActiveDirectory">
{{ "ex" | i18n }} include:Sales,IT
</div>
<div class="form-text" *ngIf="directory === directoryType.Okta">
{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"
</div>
<div class="form-text" *ngIf="directory === directoryType.GSuite">
{{ "ex" | i18n }} include:Sales,IT
</div>
<small class="text-muted form-text" *ngIf="directory === directoryType.Ldap"
>{{ "ex" | i18n }} (&amp;(objectClass=group)(!(cn=Sales*))(!(cn=IT*)))</small
>
<small
class="text-muted form-text"
*ngIf="directory === directoryType.AzureActiveDirectory"
>{{ "ex" | i18n }} include:Sales,IT</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.Okta"
>{{ "ex" | i18n }} include:Sales,IT | type eq "APP_GROUP"</small
>
<small class="text-muted form-text" *ngIf="directory === directoryType.GSuite"
>{{ "ex" | i18n }} include:Sales,IT</small
>
</div>
<div class="mb-3" [hidden]="directory != directoryType.Ldap">
<label for="groupPath" class="form-label">{{ "groupPath" | i18n }}</label>
<div class="form-group" [hidden]="directory != directoryType.Ldap">
<label for="groupPath">{{ "groupPath" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -717,14 +723,12 @@
name="GroupPath"
[(ngModel)]="sync.groupPath"
/>
<div class="form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</div>
<div class="form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</div>
<small class="text-muted form-text" *ngIf="!ldap.ad">{{ "ex" | i18n }} CN=Groups</small>
<small class="text-muted form-text" *ngIf="ldap.ad">{{ "ex" | i18n }} CN=Users</small>
</div>
<div [hidden]="directory != directoryType.Ldap || ldap.ad">
<div class="mb-3">
<label for="groupObjectClass" class="form-label">{{
"groupObjectClass" | i18n
}}</label>
<div class="form-group">
<label for="groupObjectClass">{{ "groupObjectClass" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -732,12 +736,10 @@
name="GroupObjectClass"
[(ngModel)]="sync.groupObjectClass"
/>
<div class="form-text">{{ "ex" | i18n }} groupOfUniqueNames</div>
<small class="text-muted form-text">{{ "ex" | i18n }} groupOfUniqueNames</small>
</div>
<div class="mb-3">
<label for="groupNameAttribute" class="form-label">{{
"groupNameAttribute" | i18n
}}</label>
<div class="form-group">
<label for="groupNameAttribute">{{ "groupNameAttribute" | i18n }}</label>
<input
type="text"
class="form-control"
@@ -745,7 +747,7 @@
name="GroupNameAttribute"
[(ngModel)]="sync.groupNameAttribute"
/>
<div class="form-text">{{ "ex" | i18n }} name</div>
<small class="text-muted form-text">{{ "ex" | i18n }} name</small>
</div>
</div>
</div>

View File

@@ -2,6 +2,7 @@ import * as fs from "fs";
import * as path from "path";
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 { LogLevelType } from "@/jslib/common/src/enums/logLevelType";
import { StateFactory } from "@/jslib/common/src/factories/stateFactory";
@@ -10,7 +11,12 @@ import { AppIdService } from "@/jslib/common/src/services/appId.service";
import { ContainerService } from "@/jslib/common/src/services/container.service";
import { CryptoService } from "@/jslib/common/src/services/crypto.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 { OrganizationService } from "@/jslib/common/src/services/organization.service";
import { PasswordGenerationService } from "@/jslib/common/src/services/passwordGeneration.service";
import { SearchService } from "@/jslib/common/src/services/search.service";
import { SettingsService } from "@/jslib/common/src/services/settings.service";
import { TokenService } from "@/jslib/common/src/services/token.service";
import { CliPlatformUtilsService } from "@/jslib/node/src/cli/services/cliPlatformUtils.service";
import { ConsoleLogService } from "@/jslib/node/src/cli/services/consoleLog.service";
@@ -23,6 +29,7 @@ import { AuthService } from "./services/auth.service";
import { I18nService } from "./services/i18n.service";
import { KeytarSecureStorageService } from "./services/keytarSecureStorage.service";
import { LowdbStorageService } from "./services/lowdbStorage.service";
import { NoopTwoFactorService } from "./services/noop/noopTwoFactor.service";
import { StateService } from "./services/state.service";
import { StateMigrationService } from "./services/stateMigration.service";
import { SyncService } from "./services/sync.service";
@@ -30,11 +37,10 @@ import { SyncService } from "./services/sync.service";
// eslint-disable-next-line
const packageJson = require("../package.json");
export const searchService: SearchService = null;
export class Main {
dataFilePath: string;
logService: ConsoleLogService;
program: Program;
messagingService: NoopMessagingService;
storageService: LowdbStorageService;
secureStorageService: StorageServiceAbstraction;
@@ -48,9 +54,16 @@ export class Main {
containerService: ContainerService;
cryptoFunctionService: NodeCryptoFunctionService;
authService: AuthService;
searchService: SearchService;
settingsService: SettingsService;
syncService: SyncService;
passwordGenerationService: PasswordGenerationService;
keyConnectorService: KeyConnectorService;
program: Program;
stateService: StateService;
stateMigrationService: StateMigrationService;
organizationService: OrganizationService;
twoFactorService: TwoFactorServiceAbstraction;
constructor() {
const applicationName = "Bitwarden Directory Connector";
@@ -138,12 +151,33 @@ export class Main {
);
this.containerService = new ContainerService(this.cryptoService);
this.authService = new AuthService(
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.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.keyConnectorService,
this.environmentService,
this.stateService,
this.twoFactorService,
this.i18nService,
);
this.syncService = new SyncService(
@@ -156,6 +190,15 @@ export class Main {
this.stateService,
);
this.passwordGenerationService = new PasswordGenerationService(
this.cryptoService,
this.stateService,
);
this.settingsService = new SettingsService(this.stateService);
this.searchService = new SearchService(this.logService, this.i18nService);
this.program = new Program(this);
}

View File

@@ -1,60 +0,0 @@
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,
});
});
});

View File

@@ -1,87 +0,0 @@
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(),
};
}
}

View File

@@ -9,7 +9,7 @@ import { MenuMain } from "./menu.main";
const SyncCheckInterval = 60 * 1000; // 1 minute
export class MessagingMain {
private syncTimeout: NodeJS.Timeout;
private syncTimeout: NodeJS.Timer;
constructor(
private windowMain: WindowMain,

View File

@@ -0,0 +1,66 @@
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(),
}),
);
}
}

View File

@@ -1,11 +1,9 @@
import { Entry } from "./entry";
import { UserEntry } from "./userEntry";
export class GroupEntry extends Entry {
name: string;
userMemberExternalIds = new Set<string>();
groupMemberReferenceIds = new Set<string>();
users: UserEntry[] = [];
get displayName(): string {
if (this.name == null) {

View File

@@ -5,6 +5,8 @@ import { Command, OptionValues } from "commander";
import { Utils } from "@/jslib/common/src/misc/utils";
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 { Response } from "@/jslib/node/src/cli/models/response";
import { StringResponse } from "@/jslib/node/src/cli/models/response/stringResponse";
@@ -13,8 +15,6 @@ import { Main } from "./bwdc";
import { ClearCacheCommand } from "./commands/clearCache.command";
import { ConfigCommand } from "./commands/config.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 { TestCommand } from "./commands/test.command";
@@ -92,7 +92,19 @@ export class Program extends BaseProgram {
})
.action(async (clientId: string, clientSecret: string, options: OptionValues) => {
await this.exitIfAuthed();
const command = new LoginCommand(this.main.authService);
const command = new LoginCommand(
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.twoFactorService,
"connector",
);
if (!Utils.isNullOrWhitespace(clientId)) {
process.env.BW_CLIENTID = clientId;
@@ -101,7 +113,8 @@ export class Program extends BaseProgram {
process.env.BW_CLIENTSECRET = clientSecret;
}
const response = await command.run();
options = Object.assign(options ?? {}, { apikey: true }); // force apikey use
const response = await command.run(null, null, options);
this.processResponse(response);
});
@@ -118,6 +131,7 @@ export class Program extends BaseProgram {
await this.exitIfNotAuthed();
const command = new LogoutCommand(
this.main.authService,
this.main.i18nService,
async () => await this.main.logout(),
);
const response = await command.run();

View File

@@ -138,6 +138,6 @@ ul.testing-list {
&:focus,
&.focus {
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-contrast($primary), $primary, 15%), 0.5);
box-shadow: 0 0 0 $btn-focus-width rgba(mix(color-yiq($primary), $primary, 15%), 0.5);
}
}

View File

@@ -1,91 +1,65 @@
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 { 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 { PlatformUtilsService } from "@/jslib/common/src/abstractions/platformUtils.service";
import {
AccountKeys,
AccountProfile,
AccountTokens,
} 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 { TokenService } from "@/jslib/common/src/abstractions/token.service";
import { TwoFactorService } from "@/jslib/common/src/abstractions/twoFactor.service";
import { AuthResult } from "@/jslib/common/src/models/domain/authResult";
import { ApiLogInCredentials } from "@/jslib/common/src/models/domain/logInCredentials";
import { AuthService as AuthServiceBase } from "@/jslib/common/src/services/auth.service";
import { StateService } from "../abstractions/state.service";
import { Account, DirectoryConfigurations, DirectorySettings } from "../models/account";
import { OrganizationLogInStrategy } from "../misc/logInStrategies/organizationLogIn.strategy";
export class AuthService {
export class AuthService extends AuthServiceBase {
constructor(
private apiService: ApiService,
private appIdService: AppIdService,
private platformUtilsService: PlatformUtilsService,
private messagingService: MessagingService,
private stateService: StateService,
) {}
async logIn(credentials: { clientId: string; clientSecret: string }) {
const tokenRequest = new ApiTokenRequest(
credentials.clientId,
credentials.clientSecret,
new TokenRequestTwoFactor(), // unused
await this.buildDeviceRequest(),
);
const response = await this.apiService.postIdentityToken(tokenRequest);
if (response instanceof IdentityTokenResponse) {
await this.saveAccountInformation(tokenRequest, response);
return;
}
throw new Error("Invalid response object.");
}
logOut(callback: () => void) {
callback();
this.messagingService.send("loggedOut");
}
private async buildDeviceRequest() {
const appId = await this.appIdService.getAppId();
return new DeviceRequest(appId, this.platformUtilsService);
}
private async saveAccountInformation(
tokenRequest: ApiTokenRequest,
tokenResponse: IdentityTokenResponse,
cryptoService: CryptoService,
apiService: ApiService,
tokenService: TokenService,
appIdService: AppIdService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
logService: LogService,
keyConnectorService: KeyConnectorService,
environmentService: EnvironmentService,
stateService: StateService,
twoFactorService: TwoFactorService,
i18nService: I18nService,
) {
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(),
}),
super(
cryptoService,
apiService,
tokenService,
appIdService,
platformUtilsService,
messagingService,
logService,
keyConnectorService,
environmentService,
stateService,
twoFactorService,
i18nService,
);
}
async logIn(credentials: ApiLogInCredentials): Promise<AuthResult> {
const strategy = new OrganizationLogInStrategy(
this.cryptoService,
this.apiService,
this.tokenService,
this.appIdService,
this.platformUtilsService,
this.messagingService,
this.logService,
this.stateService,
this.twoFactorService,
);
return strategy.logIn(credentials);
}
}

View File

@@ -1,97 +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 { 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(),
}),
);
});
});

View File

@@ -199,7 +199,8 @@ export class GSuiteDirectoryService extends BaseDirectoryService implements IDir
const p = Object.assign({ groupKey: group.id, pageToken: nextPageToken }, this.authParams);
const memRes = await this.service.members.list(p);
if (memRes.status !== 200) {
throw new Error("Group member list API failed: " + memRes.statusText);
this.logService.warning("Group member list API failed: " + memRes.statusText);
return entry;
}
nextPageToken = memRes.data.nextPageToken;

View File

@@ -1,7 +1,7 @@
import * as fs from "fs";
import * as tls from "tls";
import { checkServerIdentity, PeerCertificate } from "tls";
import * as ldapts from "ldapts";
import * as ldap from "ldapjs";
import { I18nService } from "@/jslib/common/src/abstractions/i18n.service";
import { LogService } from "@/jslib/common/src/abstractions/log.service";
@@ -19,7 +19,7 @@ import { IDirectoryService } from "./directory.service";
const UserControlAccountDisabled = 2;
export class LdapDirectoryService implements IDirectoryService {
private client: ldapts.Client;
private client: ldap.Client;
private dirConfig: LdapConfiguration;
private syncConfig: SyncConfiguration;
@@ -48,29 +48,25 @@ export class LdapDirectoryService implements IDirectoryService {
await this.bind();
let users: UserEntry[];
let groups: GroupEntry[];
try {
if (this.syncConfig.users) {
users = await this.getUsers(force, test);
}
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();
if (this.syncConfig.users) {
users = await this.getUsers(force);
}
let groups: GroupEntry[];
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);
}
await this.unbind();
return [groups, users];
}
private async getUsers(force: boolean, test: boolean): Promise<UserEntry[]> {
private async getUsers(force: boolean): Promise<UserEntry[]> {
const lastSync = await this.stateService.getLastUserSync();
let filter = this.buildBaseFilter(this.syncConfig.userObjectClass, this.syncConfig.userFilter);
filter = this.buildRevisionFilter(filter, force, lastSync);
@@ -81,20 +77,7 @@ export class LdapDirectoryService implements IDirectoryService {
const regularUsers = await this.search<UserEntry>(path, filter, (se: any) =>
this.buildUser(se, false),
);
// Active Directory has a special way of managing deleted users that
// standard LDAP does not. Users can be "tombstoned", where they cease to
// exist, or they can be "recycled" where they exist in a quarantined
// state for a period of time before being tombstoned.
//
// Essentially, recycled users are soft deleted but tombstoned users are
// hard deleted. In standard LDAP deleted users are only ever hard
// deleted.
//
// We check for recycled Active Directory users below, but only if the
// sync is a test sync or the "Overwrite existing users" flag is checked.
const ignoreDeletedUsers = !this.dirConfig.ad || (!force && !test);
if (ignoreDeletedUsers) {
if (!this.dirConfig.ad) {
return regularUsers;
}
@@ -105,7 +88,10 @@ export class LdapDirectoryService implements IDirectoryService {
const deletedPath = this.makeSearchPath("CN=Deleted Objects");
this.logService.info("Deleted user search: " + deletedPath + " => " + deletedFilter);
const delControl = new ldapts.Control("1.2.840.113556.1.4.417", { critical: true });
const delControl = new (ldap as any).Control({
type: "1.2.840.113556.1.4.417",
criticality: true,
});
const deletedUsers = await this.search<UserEntry>(
deletedPath,
deletedFilter,
@@ -335,93 +321,144 @@ export class LdapDirectoryService implements IDirectoryService {
path: string,
filter: string,
processEntry: (searchEntry: any) => T,
controls: ldapts.Control[] = [],
controls: ldap.Control[] = [],
): Promise<T[]> {
const options: ldapts.SearchOptions = {
const options: ldap.SearchOptions = {
filter: filter,
scope: "sub",
paged: this.dirConfig.pagedSearch,
};
const { searchEntries } = await this.client.search(path, options, controls);
return searchEntries.map((e) => processEntry(e)).filter((e) => e != null);
const entries: T[] = [];
return new Promise<T[]>((resolve, reject) => {
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> {
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
throw new Error(this.i18nService.t("dirConfigIncomplete"));
}
const protocol = "ldap" + (this.dirConfig.ssl && !this.dirConfig.startTls ? "s" : "");
const url = protocol + "://" + this.dirConfig.hostname + ":" + this.dirConfig.port;
const options: ldapts.ClientOptions = {
url: url.trim().toLowerCase(),
};
const tlsOptions: tls.ConnectionOptions = {};
if (this.dirConfig.ssl) {
if (this.dirConfig.sslAllowUnauthorized) {
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
return new Promise<void>((resolve, reject) => {
if (this.dirConfig.hostname == null || this.dirConfig.port == null) {
reject(this.i18nService.t("dirConfigIncomplete"));
return;
}
if (!this.dirConfig.startTls) {
if (
this.dirConfig.sslCaPath != null &&
this.dirConfig.sslCaPath !== "" &&
fs.existsSync(this.dirConfig.sslCaPath)
) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.sslCaPath)];
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 = {};
if (this.dirConfig.ssl) {
if (this.dirConfig.sslAllowUnauthorized) {
tlsOptions.rejectUnauthorized = !this.dirConfig.sslAllowUnauthorized;
}
if (
this.dirConfig.sslCertPath != null &&
this.dirConfig.sslCertPath !== "" &&
fs.existsSync(this.dirConfig.sslCertPath)
) {
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);
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.sslCertPath != null &&
this.dirConfig.sslCertPath !== "" &&
fs.existsSync(this.dirConfig.sslCertPath)
) {
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 {
if (
this.dirConfig.tlsCaPath != null &&
this.dirConfig.tlsCaPath !== "" &&
fs.existsSync(this.dirConfig.tlsCaPath)
) {
tlsOptions.ca = [fs.readFileSync(this.dirConfig.tlsCaPath)];
}
this.client.bind(user, pass, (err) => {
if (err != null) {
reject(err.message);
} 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 async unbind(): Promise<void> {
return new Promise((resolve, reject) => {
this.client.unbind((err) => {
if (err != null) {
reject(err);
} else {
resolve();
}
});
});
}
private bufToGuid(buf: Buffer) {
@@ -444,7 +481,7 @@ export class LdapDirectoryService implements IDirectoryService {
return guid.toLowerCase();
}
private checkServerIdentityAltNames(host: string, cert: tls.PeerCertificate) {
private checkServerIdentityAltNames(host: string, cert: PeerCertificate) {
// 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)
// Adapted from: https://github.com/auth0/ad-ldap-connector/commit/1f4dd2be6ed93dda591dd31ed5483a9b452a8d2a
@@ -460,6 +497,6 @@ export class LdapDirectoryService implements IDirectoryService {
};
}
return tls.checkServerIdentity(host, cert);
return checkServerIdentity(host, cert);
}
}

View File

@@ -0,0 +1,40 @@
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;
}
}