mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-29 12:55:21 +01:00
Merge branch 'main' into vault/pm-5273
# Conflicts: # apps/browser/src/popup/app.component.ts # libs/common/src/state-migrations/migrate.ts
This commit is contained in:
commit
0e5f66e9c7
1
.github/renovate.json
vendored
1
.github/renovate.json
vendored
@ -37,7 +37,6 @@
|
|||||||
"base64-loader",
|
"base64-loader",
|
||||||
"buffer",
|
"buffer",
|
||||||
"bufferutil",
|
"bufferutil",
|
||||||
"clean-webpack-plugin",
|
|
||||||
"copy-webpack-plugin",
|
"copy-webpack-plugin",
|
||||||
"core-js",
|
"core-js",
|
||||||
"css-loader",
|
"css-loader",
|
||||||
|
35
.github/workflows/version-auto-bump.yml
vendored
35
.github/workflows/version-auto-bump.yml
vendored
@ -6,37 +6,11 @@ on:
|
|||||||
tags:
|
tags:
|
||||||
- desktop-v**
|
- desktop-v**
|
||||||
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump-version:
|
bump-version:
|
||||||
name: Bump Desktop Version
|
name: Bump Desktop Version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Branch
|
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
|
||||||
|
|
||||||
- name: Calculate bumped version
|
|
||||||
id: version
|
|
||||||
env:
|
|
||||||
RELEASE_TAG: ${{ github.ref }}
|
|
||||||
run: |
|
|
||||||
CURR_MAJOR=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\1/')
|
|
||||||
CURR_PATCH=$(echo $RELEASE_TAG | sed -r 's/refs\/tags\/[a-z]*-v([0-9]{4}\.[0-9]{1,2})\.([0-9]{1,2})/\2/')
|
|
||||||
echo "Current Major: $CURR_MAJOR"
|
|
||||||
echo "Current Patch: $CURR_PATCH"
|
|
||||||
|
|
||||||
NEW_PATCH=$((CURR_PATCH+1))
|
|
||||||
|
|
||||||
echo "New patch: $NEW_PATCH"
|
|
||||||
|
|
||||||
NEW_VER=$CURR_MAJOR.$NEW_PATCH
|
|
||||||
echo "New Version: $NEW_VER"
|
|
||||||
echo "New Version: $NEW_VER" >> $GITHUB_STEP_SUMMARY
|
|
||||||
echo "new_version=$NEW_VER" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
with:
|
with:
|
||||||
@ -49,10 +23,13 @@ jobs:
|
|||||||
keyvault: bitwarden-ci
|
keyvault: bitwarden-ci
|
||||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: "Bump version to ${{ steps.version.outputs.new_version }}"
|
- name: Trigger Version Bump workflow
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
run: |
|
run: |
|
||||||
echo '{"cut_rc_branch": "false", "version_number": "${{ steps.version.outputs.new_version }}",
|
echo '{"cut_rc_branch": "false", \
|
||||||
"bump_browser": "false", "bump_cli": "false", "bump_desktop": "true", "bump_web": "false"}' | \
|
"bump_browser": "false", \
|
||||||
|
"bump_cli": "false", \
|
||||||
|
"bump_desktop": "true", \
|
||||||
|
"bump_web": "false"}' | \
|
||||||
gh workflow run version-bump.yml --json --repo bitwarden/clients
|
gh workflow run version-bump.yml --json --repo bitwarden/clients
|
||||||
|
265
.github/workflows/version-bump.yml
vendored
265
.github/workflows/version-bump.yml
vendored
@ -1,6 +1,5 @@
|
|||||||
---
|
---
|
||||||
name: Version Bump
|
name: Version Bump
|
||||||
run-name: Version Bump - v${{ inputs.version_number }}
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@ -21,9 +20,10 @@ on:
|
|||||||
description: "Bump Web?"
|
description: "Bump Web?"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
version_number:
|
version_number_override:
|
||||||
description: "New version (example: '2024.1.0')"
|
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
|
||||||
required: true
|
required: false
|
||||||
|
type: string
|
||||||
cut_rc_branch:
|
cut_rc_branch:
|
||||||
description: "Cut RC branch?"
|
description: "Cut RC branch?"
|
||||||
default: true
|
default: true
|
||||||
@ -31,22 +31,19 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bump_version:
|
bump_version:
|
||||||
name: "Bump Version to v${{ inputs.version_number }}"
|
name: Bump Version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
outputs:
|
||||||
|
version_browser: ${{ steps.set-final-version-output.outputs.version_browser }}
|
||||||
|
version_cli: ${{ steps.set-final-version-output.outputs.version_cli }}
|
||||||
|
version_desktop: ${{ steps.set-final-version-output.outputs.version_desktop }}
|
||||||
|
version_web: ${{ steps.set-final-version-output.outputs.version_web }}
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Azure - Prod Subscription
|
- name: Validate version input
|
||||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
if: ${{ inputs.version_number_override != '' }}
|
||||||
|
uses: bitwarden/gh-actions/version-check@main
|
||||||
with:
|
with:
|
||||||
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Retrieve secrets
|
|
||||||
id: retrieve-secrets
|
|
||||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
|
||||||
with:
|
|
||||||
keyvault: "bitwarden-ci"
|
|
||||||
secrets: "github-gpg-private-key,
|
|
||||||
github-gpg-private-key-passphrase,
|
|
||||||
github-pat-bitwarden-devops-bot-repo-scope"
|
|
||||||
|
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||||
@ -63,6 +60,20 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Login to Azure - CI Subscription
|
||||||
|
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Retrieve secrets
|
||||||
|
id: retrieve-secrets
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-ci"
|
||||||
|
secrets: "github-gpg-private-key,
|
||||||
|
github-gpg-private-key-passphrase,
|
||||||
|
github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
- name: Import GPG key
|
- name: Import GPG key
|
||||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
||||||
with:
|
with:
|
||||||
@ -71,6 +82,11 @@ jobs:
|
|||||||
git_user_signingkey: true
|
git_user_signingkey: true
|
||||||
git_commit_gpgsign: true
|
git_commit_gpgsign: true
|
||||||
|
|
||||||
|
- name: Setup git
|
||||||
|
run: |
|
||||||
|
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
||||||
|
git config --local user.name "bitwarden-devops-bot"
|
||||||
|
|
||||||
- name: Create Version Branch
|
- name: Create Version Branch
|
||||||
id: create-branch
|
id: create-branch
|
||||||
run: |
|
run: |
|
||||||
@ -90,7 +106,7 @@ jobs:
|
|||||||
printf -v joined '%s,' "${CLIENTS[@]}"
|
printf -v joined '%s,' "${CLIENTS[@]}"
|
||||||
echo "client=${joined%,}" >> $GITHUB_OUTPUT
|
echo "client=${joined%,}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
NAME=version_bump_${{ github.ref_name }}_${{ inputs.version_number }}
|
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
|
||||||
git switch -c $NAME
|
git switch -c $NAME
|
||||||
echo "name=$NAME" >> $GITHUB_OUTPUT
|
echo "name=$NAME" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
@ -99,13 +115,20 @@ jobs:
|
|||||||
########################
|
########################
|
||||||
|
|
||||||
### Browser
|
### Browser
|
||||||
- name: Browser - Verify input version
|
- name: Get current Browser version
|
||||||
if: ${{ inputs.bump_browser == true }}
|
if: ${{ inputs.bump_browser == true }}
|
||||||
env:
|
id: current-browser-version
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
working-directory: apps/browser
|
||||||
|
|
||||||
|
- name: Browser - Verify input version
|
||||||
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
|
||||||
|
env:
|
||||||
|
CURRENT_VERSION: ${{ steps.current-browser-version.outputs.version }}
|
||||||
|
NEW_VERSION: ${{ inputs.version_number_override }}
|
||||||
|
run: |
|
||||||
# Error if version has not changed.
|
# Error if version has not changed.
|
||||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
echo "Version has not changed."
|
echo "Version has not changed."
|
||||||
@ -122,23 +145,52 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
working-directory: apps/browser
|
working-directory: apps/browser
|
||||||
|
|
||||||
- name: Bump Browser Version
|
- name: Calculate next Browser release version
|
||||||
if: ${{ inputs.bump_browser == true }}
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
|
||||||
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number }}
|
id: calculate-next-browser-version
|
||||||
|
uses: bitwarden/gh-actions/version-next@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.current-browser-version.outputs.version }}
|
||||||
|
|
||||||
- name: Bump Browser Version - Manifest
|
- name: Bump Browser Version - Version Override
|
||||||
if: ${{ inputs.bump_browser == true }}
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
|
||||||
|
id: bump-browser-version-override
|
||||||
|
run: npm version --workspace=@bitwarden/browser ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
|
- name: Bump Browser Version - Automatic Calculation
|
||||||
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
|
||||||
|
id: bump-browser-version-automatic
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.calculate-next-browser-version.outputs.version }}
|
||||||
|
run: npm version --workspace=@bitwarden/browser $VERSION
|
||||||
|
|
||||||
|
- name: Bump Browser Version - Manifest - Version Override
|
||||||
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
|
||||||
uses: bitwarden/gh-actions/version-bump@main
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
|
||||||
file_path: "apps/browser/src/manifest.json"
|
file_path: "apps/browser/src/manifest.json"
|
||||||
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
- name: Bump Browser Version - Manifest v3
|
- name: Bump Browser Version - Manifest - Automatic Calculation
|
||||||
if: ${{ inputs.bump_browser == true }}
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
|
||||||
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
|
with:
|
||||||
|
file_path: "apps/browser/src/manifest.json"
|
||||||
|
version: ${{ steps.calculate-next-browser-version.outputs.version }}
|
||||||
|
|
||||||
|
- name: Bump Browser Version - Manifest v3 - Version Override
|
||||||
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override != '' }}
|
||||||
uses: bitwarden/gh-actions/version-bump@main
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
with:
|
with:
|
||||||
version: ${{ inputs.version_number }}
|
|
||||||
file_path: "apps/browser/src/manifest.v3.json"
|
file_path: "apps/browser/src/manifest.v3.json"
|
||||||
|
version: ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
|
- name: Bump Browser Version - Manifest v3 - Automatic Calculation
|
||||||
|
if: ${{ inputs.bump_browser == true && inputs.version_number_override == '' }}
|
||||||
|
uses: bitwarden/gh-actions/version-bump@main
|
||||||
|
with:
|
||||||
|
file_path: "apps/browser/src/manifest.v3.json"
|
||||||
|
version: ${{ steps.calculate-next-browser-version.outputs.version }}
|
||||||
|
|
||||||
- name: Run Prettier after Browser Version Bump
|
- name: Run Prettier after Browser Version Bump
|
||||||
if: ${{ inputs.bump_browser == true }}
|
if: ${{ inputs.bump_browser == true }}
|
||||||
@ -148,13 +200,20 @@ jobs:
|
|||||||
prettier --write apps/browser/src/manifest.v3.json
|
prettier --write apps/browser/src/manifest.v3.json
|
||||||
|
|
||||||
### CLI
|
### CLI
|
||||||
- name: CLI - Verify input version
|
- name: Get current CLI version
|
||||||
if: ${{ inputs.bump_cli == true }}
|
if: ${{ inputs.bump_cli == true }}
|
||||||
env:
|
id: current-cli-version
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
working-directory: apps/cli
|
||||||
|
|
||||||
|
- name: CLI - Verify input version
|
||||||
|
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
|
||||||
|
env:
|
||||||
|
CURRENT_VERSION: ${{ steps.current-cli-version.outputs.version }}
|
||||||
|
NEW_VERSION: ${{ inputs.version_number_override }}
|
||||||
|
run: |
|
||||||
# Error if version has not changed.
|
# Error if version has not changed.
|
||||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
echo "Version has not changed."
|
echo "Version has not changed."
|
||||||
@ -171,18 +230,40 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
working-directory: apps/cli
|
working-directory: apps/cli
|
||||||
|
|
||||||
- name: Bump CLI Version
|
- name: Calculate next CLI release version
|
||||||
if: ${{ inputs.bump_cli == true }}
|
if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
|
||||||
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number }}
|
id: calculate-next-cli-version
|
||||||
|
uses: bitwarden/gh-actions/version-next@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.current-cli-version.outputs.version }}
|
||||||
|
|
||||||
|
- name: Bump CLI Version - Version Override
|
||||||
|
if: ${{ inputs.bump_cli == true && inputs.version_number_override != '' }}
|
||||||
|
id: bump-cli-version-override
|
||||||
|
run: npm version --workspace=@bitwarden/cli ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
|
- name: Bump CLI Version - Automatic Calculation
|
||||||
|
if: ${{ inputs.bump_cli == true && inputs.version_number_override == '' }}
|
||||||
|
id: bump-cli-version-automatic
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.calculate-next-cli-version.outputs.version }}
|
||||||
|
run: npm version --workspace=@bitwarden/cli $VERSION
|
||||||
|
|
||||||
### Desktop
|
### Desktop
|
||||||
- name: Desktop - Verify input version
|
- name: Get current Desktop version
|
||||||
if: ${{ inputs.bump_desktop == true }}
|
if: ${{ inputs.bump_desktop == true }}
|
||||||
env:
|
id: current-desktop-version
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
working-directory: apps/desktop
|
||||||
|
|
||||||
|
- name: Desktop - Verify input version
|
||||||
|
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
|
||||||
|
env:
|
||||||
|
CURRENT_VERSION: ${{ steps.current-desktop-version.outputs.version }}
|
||||||
|
NEW_VERSION: ${{ inputs.version_number_override }}
|
||||||
|
run: |
|
||||||
# Error if version has not changed.
|
# Error if version has not changed.
|
||||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
echo "Version has not changed."
|
echo "Version has not changed."
|
||||||
@ -199,23 +280,52 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
working-directory: apps/desktop
|
working-directory: apps/desktop
|
||||||
|
|
||||||
- name: Bump Desktop Version - Root
|
- name: Calculate next Desktop release version
|
||||||
if: ${{ inputs.bump_desktop == true }}
|
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
|
||||||
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number }}
|
id: calculate-next-desktop-version
|
||||||
|
uses: bitwarden/gh-actions/version-next@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.current-desktop-version.outputs.version }}
|
||||||
|
|
||||||
- name: Bump Desktop Version - App
|
- name: Bump Desktop Version - Root - Version Override
|
||||||
if: ${{ inputs.bump_desktop == true }}
|
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
|
||||||
run: npm version ${{ inputs.version_number }}
|
id: bump-desktop-version-override
|
||||||
|
run: npm version --workspace=@bitwarden/desktop ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
|
- name: Bump Desktop Version - Root - Automatic Calculation
|
||||||
|
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
|
||||||
|
id: bump-desktop-version-automatic
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
|
||||||
|
run: npm version --workspace=@bitwarden/desktop $VERSION
|
||||||
|
|
||||||
|
- name: Bump Desktop Version - App - Version Override
|
||||||
|
if: ${{ inputs.bump_desktop == true && inputs.version_number_override != '' }}
|
||||||
|
run: npm version ${{ inputs.version_number_override }}
|
||||||
|
working-directory: "apps/desktop/src"
|
||||||
|
|
||||||
|
- name: Bump Desktop Version - App - Automatic Calculation
|
||||||
|
if: ${{ inputs.bump_desktop == true && inputs.version_number_override == '' }}
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.calculate-next-desktop-version.outputs.version }}
|
||||||
|
run: npm version $VERSION
|
||||||
working-directory: "apps/desktop/src"
|
working-directory: "apps/desktop/src"
|
||||||
|
|
||||||
### Web
|
### Web
|
||||||
- name: Web - Verify input version
|
- name: Get current Web version
|
||||||
if: ${{ inputs.bump_web == true }}
|
if: ${{ inputs.bump_web == true }}
|
||||||
env:
|
id: current-web-version
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
|
||||||
run: |
|
run: |
|
||||||
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
CURRENT_VERSION=$(cat package.json | jq -r '.version')
|
||||||
|
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
|
||||||
|
working-directory: apps/web
|
||||||
|
|
||||||
|
- name: Web - Verify input version
|
||||||
|
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
|
||||||
|
env:
|
||||||
|
CURRENT_VERSION: ${{ steps.current-web-version.outputs.version }}
|
||||||
|
NEW_VERSION: ${{ inputs.version_number_override }}
|
||||||
|
run: |
|
||||||
# Error if version has not changed.
|
# Error if version has not changed.
|
||||||
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
|
||||||
echo "Version has not changed."
|
echo "Version has not changed."
|
||||||
@ -232,16 +342,47 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
working-directory: apps/web
|
working-directory: apps/web
|
||||||
|
|
||||||
- name: Bump Web Version
|
- name: Calculate next Web release version
|
||||||
if: ${{ inputs.bump_web == true }}
|
if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
|
||||||
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number }}
|
id: calculate-next-web-version
|
||||||
|
uses: bitwarden/gh-actions/version-next@main
|
||||||
|
with:
|
||||||
|
version: ${{ steps.current-web-version.outputs.version }}
|
||||||
|
|
||||||
|
- name: Bump Web Version - Version Override
|
||||||
|
if: ${{ inputs.bump_web == true && inputs.version_number_override != '' }}
|
||||||
|
id: bump-web-version-override
|
||||||
|
run: npm version --workspace=@bitwarden/web-vault ${{ inputs.version_number_override }}
|
||||||
|
|
||||||
|
- name: Bump Web Version - Automatic Calculation
|
||||||
|
if: ${{ inputs.bump_web == true && inputs.version_number_override == '' }}
|
||||||
|
id: bump-web-version-automatic
|
||||||
|
env:
|
||||||
|
VERSION: ${{ steps.calculate-next-web-version.outputs.version }}
|
||||||
|
run: npm version --workspace=@bitwarden/web-vault $VERSION
|
||||||
|
|
||||||
########################
|
########################
|
||||||
|
|
||||||
- name: Setup git
|
- name: Set final version output
|
||||||
|
id: set-final-version-output
|
||||||
run: |
|
run: |
|
||||||
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
|
if [[ "${{ steps.bump-browser-version-override.outcome }}" = "success" ]]; then
|
||||||
git config --local user.name "bitwarden-devops-bot"
|
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-browser-version-automatic.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ steps.calculate-next-browser-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-cli-version-override.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-cli-version-automatic.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ steps.calculate-next-cli-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-desktop-version-override.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-desktop-version-automatic.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ steps.calculate-next-desktop-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-web-version-override.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
|
||||||
|
elif [[ "${{ steps.bump-web-version-automatic.outcome }}" = "success" ]]; then
|
||||||
|
echo "version=${{ steps.calculate-next-web-version.outputs.version }}" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Check if version changed
|
- name: Check if version changed
|
||||||
id: version-changed
|
id: version-changed
|
||||||
@ -257,7 +398,7 @@ jobs:
|
|||||||
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
|
||||||
env:
|
env:
|
||||||
CLIENT: ${{ steps.create-branch.outputs.client }}
|
CLIENT: ${{ steps.create-branch.outputs.client }}
|
||||||
VERSION: ${{ inputs.version_number }}
|
VERSION: ${{ steps.set-final-version-output.outputs.version }}
|
||||||
run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a
|
run: git commit -m "Bumped ${CLIENT} version to ${VERSION}" -a
|
||||||
|
|
||||||
- name: Push changes
|
- name: Push changes
|
||||||
@ -272,7 +413,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
GH_TOKEN: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
|
||||||
TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ inputs.version_number }}"
|
TITLE: "Bump ${{ steps.create-branch.outputs.client }} version to ${{ steps.set-final-version-output.outputs.version }}"
|
||||||
run: |
|
run: |
|
||||||
PR_URL=$(gh pr create --title "$TITLE" \
|
PR_URL=$(gh pr create --title "$TITLE" \
|
||||||
--base "main" \
|
--base "main" \
|
||||||
@ -288,7 +429,7 @@ jobs:
|
|||||||
- [X] Other
|
- [X] Other
|
||||||
|
|
||||||
## Objective
|
## Objective
|
||||||
Automated ${{ steps.create-branch.outputs.client }} version bump to ${{ inputs.version_number }}")
|
Automated ${{ steps.create-branch.outputs.client }} version bump to ${{ steps.set-final-version-output.outputs.version }}")
|
||||||
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Approve PR
|
- name: Approve PR
|
||||||
@ -307,8 +448,8 @@ jobs:
|
|||||||
|
|
||||||
cut_rc:
|
cut_rc:
|
||||||
name: Cut RC branch
|
name: Cut RC branch
|
||||||
needs: bump_version
|
|
||||||
if: ${{ inputs.cut_rc_branch == true }}
|
if: ${{ inputs.cut_rc_branch == true }}
|
||||||
|
needs: bump_version
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Branch
|
- name: Checkout Branch
|
||||||
@ -320,7 +461,7 @@ jobs:
|
|||||||
- name: Browser - Verify version has been updated
|
- name: Browser - Verify version has been updated
|
||||||
if: ${{ inputs.bump_browser == true }}
|
if: ${{ inputs.bump_browser == true }}
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
NEW_VERSION: ${{ needs.bump_version.outputs.version_browser }}
|
||||||
run: |
|
run: |
|
||||||
# Wait for version to change.
|
# Wait for version to change.
|
||||||
while : ; do
|
while : ; do
|
||||||
@ -338,7 +479,7 @@ jobs:
|
|||||||
- name: CLI - Verify version has been updated
|
- name: CLI - Verify version has been updated
|
||||||
if: ${{ inputs.bump_cli == true }}
|
if: ${{ inputs.bump_cli == true }}
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
NEW_VERSION: ${{ needs.bump_version.outputs.version_cli }}
|
||||||
run: |
|
run: |
|
||||||
# Wait for version to change.
|
# Wait for version to change.
|
||||||
while : ; do
|
while : ; do
|
||||||
@ -356,7 +497,7 @@ jobs:
|
|||||||
- name: Desktop - Verify version has been updated
|
- name: Desktop - Verify version has been updated
|
||||||
if: ${{ inputs.bump_desktop == true }}
|
if: ${{ inputs.bump_desktop == true }}
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
NEW_VERSION: ${{ needs.bump_version.outputs.version_desktop }}
|
||||||
run: |
|
run: |
|
||||||
# Wait for version to change.
|
# Wait for version to change.
|
||||||
while : ; do
|
while : ; do
|
||||||
@ -374,7 +515,7 @@ jobs:
|
|||||||
- name: Web - Verify version has been updated
|
- name: Web - Verify version has been updated
|
||||||
if: ${{ inputs.bump_web == true }}
|
if: ${{ inputs.bump_web == true }}
|
||||||
env:
|
env:
|
||||||
NEW_VERSION: ${{ inputs.version_number }}
|
NEW_VERSION: ${{ needs.bump_version.outputs.version_web }}
|
||||||
run: |
|
run: |
|
||||||
# Wait for version to change.
|
# Wait for version to change.
|
||||||
while : ; do
|
while : ; do
|
||||||
|
@ -26,6 +26,10 @@ import {
|
|||||||
factory,
|
factory,
|
||||||
FactoryOptions,
|
FactoryOptions,
|
||||||
} from "../../../platform/background/service-factories/factory-options";
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import {
|
||||||
|
globalStateProviderFactory,
|
||||||
|
GlobalStateProviderInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/global-state-provider.factory";
|
||||||
import {
|
import {
|
||||||
i18nServiceFactory,
|
i18nServiceFactory,
|
||||||
I18nServiceInitOptions,
|
I18nServiceInitOptions,
|
||||||
@ -84,7 +88,8 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions
|
|||||||
PolicyServiceInitOptions &
|
PolicyServiceInitOptions &
|
||||||
PasswordStrengthServiceInitOptions &
|
PasswordStrengthServiceInitOptions &
|
||||||
DeviceTrustCryptoServiceInitOptions &
|
DeviceTrustCryptoServiceInitOptions &
|
||||||
AuthRequestServiceInitOptions;
|
AuthRequestServiceInitOptions &
|
||||||
|
GlobalStateProviderInitOptions;
|
||||||
|
|
||||||
export function loginStrategyServiceFactory(
|
export function loginStrategyServiceFactory(
|
||||||
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
|
cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices,
|
||||||
@ -113,6 +118,7 @@ export function loginStrategyServiceFactory(
|
|||||||
await policyServiceFactory(cache, opts),
|
await policyServiceFactory(cache, opts),
|
||||||
await deviceTrustCryptoServiceFactory(cache, opts),
|
await deviceTrustCryptoServiceFactory(cache, opts),
|
||||||
await authRequestServiceFactory(cache, opts),
|
await authRequestServiceFactory(cache, opts),
|
||||||
|
await globalStateProviderFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
|
||||||
@ -111,6 +112,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
|
|||||||
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
|
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
|
||||||
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
||||||
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
||||||
|
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
||||||
getWebVaultUrlForNotification: () => string;
|
getWebVaultUrlForNotification: () => string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs";
|
|||||||
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/services/auth.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
|
||||||
@ -47,6 +48,7 @@ describe("NotificationBackground", () => {
|
|||||||
const folderService = mock<FolderService>();
|
const folderService = mock<FolderService>();
|
||||||
const stateService = mock<BrowserStateService>();
|
const stateService = mock<BrowserStateService>();
|
||||||
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
||||||
|
const domainSettingsService = mock<DomainSettingsService>();
|
||||||
const environmentService = mock<EnvironmentService>();
|
const environmentService = mock<EnvironmentService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
|
|
||||||
@ -59,6 +61,7 @@ describe("NotificationBackground", () => {
|
|||||||
folderService,
|
folderService,
|
||||||
stateService,
|
stateService,
|
||||||
userNotificationSettingsService,
|
userNotificationSettingsService,
|
||||||
|
domainSettingsService,
|
||||||
environmentService,
|
environmentService,
|
||||||
logService,
|
logService,
|
||||||
);
|
);
|
||||||
|
@ -5,7 +5,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
|||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants";
|
import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
|
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
@ -60,6 +62,7 @@ export default class NotificationBackground {
|
|||||||
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
|
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
|
||||||
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
|
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
|
||||||
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
|
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
|
||||||
|
bgGetExcludedDomains: () => this.getExcludedDomains(),
|
||||||
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
|
getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -71,6 +74,7 @@ export default class NotificationBackground {
|
|||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
private stateService: BrowserStateService,
|
private stateService: BrowserStateService,
|
||||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
private environmentService: EnvironmentService,
|
private environmentService: EnvironmentService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
) {}
|
) {}
|
||||||
@ -99,6 +103,13 @@ export default class NotificationBackground {
|
|||||||
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
|
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the neverDomains setting from the domain settings service.
|
||||||
|
*/
|
||||||
|
async getExcludedDomains(): Promise<NeverDomains> {
|
||||||
|
return await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks the notification queue for any messages that need to be sent to the
|
* Checks the notification queue for any messages that need to be sent to the
|
||||||
* specified tab. If no tab is specified, the current tab will be used.
|
* specified tab. If no tab is specified, the current tab will be used.
|
||||||
|
@ -17,8 +17,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
|||||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
import BrowserPlatformUtilsService from "../../platform/services/browser-platform-utils.service";
|
|
||||||
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
||||||
|
import { BrowserPlatformUtilsService } from "../../platform/services/platform-utils/browser-platform-utils.service";
|
||||||
import { AutofillService } from "../services/abstractions/autofill.service";
|
import { AutofillService } from "../services/abstractions/autofill.service";
|
||||||
import {
|
import {
|
||||||
createAutofillPageDetailsMock,
|
createAutofillPageDetailsMock,
|
||||||
|
@ -6,10 +6,6 @@ import {
|
|||||||
EventCollectionServiceInitOptions,
|
EventCollectionServiceInitOptions,
|
||||||
eventCollectionServiceFactory,
|
eventCollectionServiceFactory,
|
||||||
} from "../../../background/service-factories/event-collection-service.factory";
|
} from "../../../background/service-factories/event-collection-service.factory";
|
||||||
import {
|
|
||||||
settingsServiceFactory,
|
|
||||||
SettingsServiceInitOptions,
|
|
||||||
} from "../../../background/service-factories/settings-service.factory";
|
|
||||||
import {
|
import {
|
||||||
CachedServices,
|
CachedServices,
|
||||||
factory,
|
factory,
|
||||||
@ -38,6 +34,10 @@ import {
|
|||||||
AutofillSettingsServiceInitOptions,
|
AutofillSettingsServiceInitOptions,
|
||||||
autofillSettingsServiceFactory,
|
autofillSettingsServiceFactory,
|
||||||
} from "./autofill-settings-service.factory";
|
} from "./autofill-settings-service.factory";
|
||||||
|
import {
|
||||||
|
DomainSettingsServiceInitOptions,
|
||||||
|
domainSettingsServiceFactory,
|
||||||
|
} from "./domain-settings-service.factory";
|
||||||
|
|
||||||
type AutoFillServiceOptions = FactoryOptions;
|
type AutoFillServiceOptions = FactoryOptions;
|
||||||
|
|
||||||
@ -48,8 +48,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
|
|||||||
TotpServiceInitOptions &
|
TotpServiceInitOptions &
|
||||||
EventCollectionServiceInitOptions &
|
EventCollectionServiceInitOptions &
|
||||||
LogServiceInitOptions &
|
LogServiceInitOptions &
|
||||||
SettingsServiceInitOptions &
|
UserVerificationServiceInitOptions &
|
||||||
UserVerificationServiceInitOptions;
|
DomainSettingsServiceInitOptions;
|
||||||
|
|
||||||
export function autofillServiceFactory(
|
export function autofillServiceFactory(
|
||||||
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
|
cache: { autofillService?: AbstractAutoFillService } & CachedServices,
|
||||||
@ -67,7 +67,7 @@ export function autofillServiceFactory(
|
|||||||
await totpServiceFactory(cache, opts),
|
await totpServiceFactory(cache, opts),
|
||||||
await eventCollectionServiceFactory(cache, opts),
|
await eventCollectionServiceFactory(cache, opts),
|
||||||
await logServiceFactory(cache, opts),
|
await logServiceFactory(cache, opts),
|
||||||
await settingsServiceFactory(cache, opts),
|
await domainSettingsServiceFactory(cache, opts),
|
||||||
await userVerificationServiceFactory(cache, opts),
|
await userVerificationServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
import { DefaultDomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CachedServices,
|
||||||
|
factory,
|
||||||
|
FactoryOptions,
|
||||||
|
} from "../../../platform/background/service-factories/factory-options";
|
||||||
|
import {
|
||||||
|
stateProviderFactory,
|
||||||
|
StateProviderInitOptions,
|
||||||
|
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||||
|
|
||||||
|
export type DomainSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
|
||||||
|
|
||||||
|
export function domainSettingsServiceFactory(
|
||||||
|
cache: { domainSettingsService?: DefaultDomainSettingsService } & CachedServices,
|
||||||
|
opts: DomainSettingsServiceInitOptions,
|
||||||
|
): Promise<DefaultDomainSettingsService> {
|
||||||
|
return factory(
|
||||||
|
cache,
|
||||||
|
"domainSettingsService",
|
||||||
|
opts,
|
||||||
|
async () => new DefaultDomainSettingsService(await stateProviderFactory(cache, opts)),
|
||||||
|
);
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { UriMatchType } from "@bitwarden/common/vault/enums";
|
|
||||||
|
|
||||||
import { BrowserApi } from "../../platform/browser/browser-api";
|
import { BrowserApi } from "../../platform/browser/browser-api";
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ export default class WebRequestBackground {
|
|||||||
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
const ciphers = await this.cipherService.getAllDecryptedForUrl(
|
||||||
domain,
|
domain,
|
||||||
null,
|
null,
|
||||||
UriMatchType.Host,
|
UriMatchStrategy.Host,
|
||||||
);
|
);
|
||||||
if (ciphers == null || ciphers.length !== 1) {
|
if (ciphers == null || ciphers.length !== 1) {
|
||||||
error();
|
error();
|
||||||
|
@ -6,7 +6,7 @@ import AutofillField from "../models/autofill-field";
|
|||||||
import { WatchedForm } from "../models/watched-form";
|
import { WatchedForm } from "../models/watched-form";
|
||||||
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
|
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
|
||||||
import { FormData } from "../services/abstractions/autofill.service";
|
import { FormData } from "../services/abstractions/autofill.service";
|
||||||
import { GlobalSettings, UserSettings } from "../types";
|
import { UserSettings } from "../types";
|
||||||
import {
|
import {
|
||||||
getFromLocalStorage,
|
getFromLocalStorage,
|
||||||
sendExtensionMessage,
|
sendExtensionMessage,
|
||||||
@ -94,10 +94,11 @@ async function loadNotificationBar() {
|
|||||||
"bgGetEnableChangedPasswordPrompt",
|
"bgGetEnableChangedPasswordPrompt",
|
||||||
);
|
);
|
||||||
const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
|
const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
|
||||||
|
const excludedDomains = await sendExtensionMessage("bgGetExcludedDomains");
|
||||||
|
|
||||||
let showNotificationBar = true;
|
let showNotificationBar = true;
|
||||||
// Look up the active user id from storage
|
// Look up the active user id from storage
|
||||||
const activeUserIdKey = "activeUserId";
|
const activeUserIdKey = "activeUserId";
|
||||||
const globalStorageKey = "global";
|
|
||||||
let activeUserId: string;
|
let activeUserId: string;
|
||||||
|
|
||||||
const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey);
|
const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey);
|
||||||
@ -109,9 +110,6 @@ async function loadNotificationBar() {
|
|||||||
const userSettingsStorageValue = await getFromLocalStorage(activeUserId);
|
const userSettingsStorageValue = await getFromLocalStorage(activeUserId);
|
||||||
if (userSettingsStorageValue[activeUserId]) {
|
if (userSettingsStorageValue[activeUserId]) {
|
||||||
const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings;
|
const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings;
|
||||||
const globalSettings: GlobalSettings = (await getFromLocalStorage(globalStorageKey))[
|
|
||||||
globalStorageKey
|
|
||||||
];
|
|
||||||
|
|
||||||
// Do not show the notification bar on the Bitwarden vault
|
// Do not show the notification bar on the Bitwarden vault
|
||||||
// because they can add logins and change passwords there
|
// because they can add logins and change passwords there
|
||||||
@ -122,8 +120,8 @@ async function loadNotificationBar() {
|
|||||||
// show the notification bar on (for login detail collection or password change).
|
// show the notification bar on (for login detail collection or password change).
|
||||||
// It is managed in the Settings > Excluded Domains page in the browser extension.
|
// It is managed in the Settings > Excluded Domains page in the browser extension.
|
||||||
// Example: '{"bitwarden.com":null}'
|
// Example: '{"bitwarden.com":null}'
|
||||||
const excludedDomainsDict = globalSettings.neverDomains;
|
|
||||||
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) {
|
if (!excludedDomains || !(window.location.hostname in excludedDomains)) {
|
||||||
if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
|
if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
|
||||||
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
|
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
|
||||||
handlePageChange();
|
handlePageChange();
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import { Component, OnInit } from "@angular/core";
|
import { Component, OnInit } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
|
||||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
||||||
|
import {
|
||||||
|
UriMatchStrategy,
|
||||||
|
UriMatchStrategySetting,
|
||||||
|
} from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
|
||||||
import { UriMatchType } from "@bitwarden/common/vault/enums";
|
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserApi } from "../../../platform/browser/browser-api";
|
import { BrowserApi } from "../../../platform/browser/browser-api";
|
||||||
@ -28,16 +30,15 @@ export class AutofillComponent implements OnInit {
|
|||||||
enableAutoFillOnPageLoad = false;
|
enableAutoFillOnPageLoad = false;
|
||||||
autoFillOnPageLoadDefault = false;
|
autoFillOnPageLoadDefault = false;
|
||||||
autoFillOnPageLoadOptions: any[];
|
autoFillOnPageLoadOptions: any[];
|
||||||
defaultUriMatch = UriMatchType.Domain;
|
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
||||||
uriMatchOptions: any[];
|
uriMatchOptions: any[];
|
||||||
autofillKeyboardHelperText: string;
|
autofillKeyboardHelperText: string;
|
||||||
accountSwitcherEnabled = false;
|
accountSwitcherEnabled = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private settingsService: SettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private autofillService: AutofillService,
|
private autofillService: AutofillService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
@ -61,12 +62,12 @@ export class AutofillComponent implements OnInit {
|
|||||||
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false },
|
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false },
|
||||||
];
|
];
|
||||||
this.uriMatchOptions = [
|
this.uriMatchOptions = [
|
||||||
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain },
|
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||||
{ name: i18nService.t("host"), value: UriMatchType.Host },
|
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||||
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith },
|
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
||||||
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression },
|
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
||||||
{ name: i18nService.t("exact"), value: UriMatchType.Exact },
|
{ name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
|
||||||
{ name: i18nService.t("never"), value: UriMatchType.Never },
|
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
|
||||||
];
|
];
|
||||||
|
|
||||||
this.accountSwitcherEnabled = enableAccountSwitching();
|
this.accountSwitcherEnabled = enableAccountSwitching();
|
||||||
@ -94,8 +95,10 @@ export class AutofillComponent implements OnInit {
|
|||||||
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
this.autofillSettingsService.autofillOnPageLoadDefault$,
|
||||||
);
|
);
|
||||||
|
|
||||||
const defaultUriMatch = await this.stateService.getDefaultUriMatch();
|
const defaultUriMatch = await firstValueFrom(
|
||||||
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch;
|
this.domainSettingsService.defaultUriMatchStrategy$,
|
||||||
|
);
|
||||||
|
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
|
||||||
|
|
||||||
const command = await this.platformUtilsService.getAutofillKeyboardShortcut();
|
const command = await this.platformUtilsService.getAutofillKeyboardShortcut();
|
||||||
await this.setAutofillKeyboardHelperText(command);
|
await this.setAutofillKeyboardHelperText(command);
|
||||||
@ -119,7 +122,7 @@ export class AutofillComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveDefaultUriMatch() {
|
async saveDefaultUriMatch() {
|
||||||
await this.stateService.setDefaultUriMatch(this.defaultUriMatch);
|
await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async setAutofillKeyboardHelperText(command: string) {
|
private async setAutofillKeyboardHelperText(command: string) {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums";
|
import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
import AutofillField from "../../models/autofill-field";
|
import AutofillField from "../../models/autofill-field";
|
||||||
@ -40,7 +41,7 @@ export interface GenerateFillScriptOptions {
|
|||||||
allowTotpAutofill: boolean;
|
allowTotpAutofill: boolean;
|
||||||
cipher: CipherView;
|
cipher: CipherView;
|
||||||
tabUrl: string;
|
tabUrl: string;
|
||||||
defaultUriMatch: UriMatchType;
|
defaultUriMatch: UriMatchStrategySetting;
|
||||||
}
|
}
|
||||||
|
|
||||||
export abstract class AutofillService {
|
export abstract class AutofillService {
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import { mock, mockReset } from "jest-mock-extended";
|
import { mock, mockReset } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||||
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
|
||||||
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
|
||||||
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
|
||||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
|
||||||
import {
|
import {
|
||||||
FieldType,
|
DefaultDomainSettingsService,
|
||||||
LinkedIdType,
|
DomainSettingsService,
|
||||||
LoginLinkedId,
|
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
UriMatchType,
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
CipherType,
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||||
} from "@bitwarden/common/vault/enums";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
|
||||||
|
import {
|
||||||
|
FakeStateProvider,
|
||||||
|
FakeAccountService,
|
||||||
|
mockAccountServiceWith,
|
||||||
|
} from "@bitwarden/common/spec";
|
||||||
|
import { UserId } from "@bitwarden/common/types/guid";
|
||||||
|
import { FieldType, LinkedIdType, LoginLinkedId, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
@ -47,15 +53,24 @@ import {
|
|||||||
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
|
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
|
||||||
import AutofillService from "./autofill.service";
|
import AutofillService from "./autofill.service";
|
||||||
|
|
||||||
|
const mockEquivalentDomains = [
|
||||||
|
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
|
||||||
|
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
|
||||||
|
["example.co.uk", "exampleapp.co.uk"],
|
||||||
|
];
|
||||||
|
|
||||||
describe("AutofillService", () => {
|
describe("AutofillService", () => {
|
||||||
let autofillService: AutofillService;
|
let autofillService: AutofillService;
|
||||||
const cipherService = mock<CipherService>();
|
const cipherService = mock<CipherService>();
|
||||||
const stateService = mock<BrowserStateService>();
|
const stateService = mock<BrowserStateService>();
|
||||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
|
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
||||||
|
let domainSettingsService: DomainSettingsService;
|
||||||
const totpService = mock<TotpService>();
|
const totpService = mock<TotpService>();
|
||||||
const eventCollectionService = mock<EventCollectionService>();
|
const eventCollectionService = mock<EventCollectionService>();
|
||||||
const logService = mock<LogService>();
|
const logService = mock<LogService>();
|
||||||
const settingsService = mock<SettingsService>();
|
|
||||||
const userVerificationService = mock<UserVerificationService>();
|
const userVerificationService = mock<UserVerificationService>();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -66,9 +81,12 @@ describe("AutofillService", () => {
|
|||||||
totpService,
|
totpService,
|
||||||
eventCollectionService,
|
eventCollectionService,
|
||||||
logService,
|
logService,
|
||||||
settingsService,
|
domainSettingsService,
|
||||||
userVerificationService,
|
userVerificationService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||||
|
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -407,6 +425,8 @@ describe("AutofillService", () => {
|
|||||||
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
|
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
|
||||||
autofillOptions.cipher.login.username = "username";
|
autofillOptions.cipher.login.username = "username";
|
||||||
autofillOptions.cipher.login.password = "password";
|
autofillOptions.cipher.login.password = "password";
|
||||||
|
|
||||||
|
jest.spyOn(autofillService, "getDefaultUriMatchStrategy").mockResolvedValue(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("given a set of autofill options that are incomplete", () => {
|
describe("given a set of autofill options that are incomplete", () => {
|
||||||
@ -468,7 +488,6 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
it("will autofill login data for a page", async () => {
|
it("will autofill login data for a page", async () => {
|
||||||
jest.spyOn(stateService, "getCanAccessPremium");
|
jest.spyOn(stateService, "getCanAccessPremium");
|
||||||
jest.spyOn(stateService, "getDefaultUriMatch");
|
|
||||||
jest.spyOn(autofillService as any, "generateFillScript");
|
jest.spyOn(autofillService as any, "generateFillScript");
|
||||||
jest.spyOn(autofillService as any, "generateLoginFillScript");
|
jest.spyOn(autofillService as any, "generateLoginFillScript");
|
||||||
jest.spyOn(logService, "info");
|
jest.spyOn(logService, "info");
|
||||||
@ -479,7 +498,7 @@ describe("AutofillService", () => {
|
|||||||
|
|
||||||
const currentAutofillPageDetails = autofillOptions.pageDetails[0];
|
const currentAutofillPageDetails = autofillOptions.pageDetails[0];
|
||||||
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
|
expect(stateService.getCanAccessPremium).toHaveBeenCalled();
|
||||||
expect(stateService.getDefaultUriMatch).toHaveBeenCalled();
|
expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled();
|
||||||
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
|
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
|
||||||
currentAutofillPageDetails.details,
|
currentAutofillPageDetails.details,
|
||||||
{
|
{
|
||||||
@ -1488,7 +1507,7 @@ describe("AutofillService", () => {
|
|||||||
};
|
};
|
||||||
defaultLoginUriView = mock<LoginUriView>({
|
defaultLoginUriView = mock<LoginUriView>({
|
||||||
uri: "https://www.example.com",
|
uri: "https://www.example.com",
|
||||||
match: UriMatchType.Domain,
|
match: UriMatchStrategy.Domain,
|
||||||
});
|
});
|
||||||
options = createGenerateFillScriptOptionsMock();
|
options = createGenerateFillScriptOptionsMock();
|
||||||
options.cipher.login = mock<LoginView>({
|
options.cipher.login = mock<LoginView>({
|
||||||
@ -1559,13 +1578,13 @@ describe("AutofillService", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips adding any login uri views that have a UriMatchType of Never to the list of saved urls", async () => {
|
it("skips adding any login uri views that have a UriMatchStrategySetting of Never to the list of saved urls", async () => {
|
||||||
const secondUriView = mock<LoginUriView>({
|
const secondUriView = mock<LoginUriView>({
|
||||||
uri: "https://www.second-example.com",
|
uri: "https://www.second-example.com",
|
||||||
});
|
});
|
||||||
const thirdUriView = mock<LoginUriView>({
|
const thirdUriView = mock<LoginUriView>({
|
||||||
uri: "https://www.third-example.com",
|
uri: "https://www.third-example.com",
|
||||||
match: UriMatchType.Never,
|
match: UriMatchStrategy.Never,
|
||||||
});
|
});
|
||||||
options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView];
|
options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView];
|
||||||
|
|
||||||
@ -2752,31 +2771,32 @@ describe("AutofillService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("inUntrustedIframe", () => {
|
describe("inUntrustedIframe", () => {
|
||||||
it("returns a false value if the passed pageUrl is equal to the options tabUrl", () => {
|
it("returns a false value if the passed pageUrl is equal to the options tabUrl", async () => {
|
||||||
const pageUrl = "https://www.example.com";
|
const pageUrl = "https://www.example.com";
|
||||||
const tabUrl = "https://www.example.com";
|
const tabUrl = "https://www.example.com";
|
||||||
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
|
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
|
||||||
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
|
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
|
||||||
jest.spyOn(settingsService, "getEquivalentDomains");
|
|
||||||
|
|
||||||
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
|
const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
|
||||||
|
|
||||||
expect(settingsService.getEquivalentDomains).not.toHaveBeenCalled();
|
|
||||||
expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled();
|
expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled();
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a false value if the passed pageUrl matches the domain of the tabUrl", () => {
|
it("returns a false value if the passed pageUrl matches the domain of the tabUrl", async () => {
|
||||||
const pageUrl = "https://subdomain.example.com";
|
const pageUrl = "https://subdomain.example.com";
|
||||||
const tabUrl = "https://www.example.com";
|
const tabUrl = "https://www.example.com";
|
||||||
const equivalentDomains = new Set(["example.com"]);
|
const equivalentDomains = new Set([
|
||||||
|
"ejemplo.es",
|
||||||
|
"example.co.uk",
|
||||||
|
"example.com",
|
||||||
|
"exampleapp.com",
|
||||||
|
]);
|
||||||
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
|
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
|
||||||
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
|
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
|
||||||
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
|
|
||||||
|
|
||||||
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
|
const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
|
||||||
|
|
||||||
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
|
|
||||||
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
|
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
|
||||||
pageUrl,
|
pageUrl,
|
||||||
equivalentDomains,
|
equivalentDomains,
|
||||||
@ -2785,17 +2805,21 @@ describe("AutofillService", () => {
|
|||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", () => {
|
it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", async () => {
|
||||||
|
const equivalentDomains = new Set([
|
||||||
|
"ejemplo.es",
|
||||||
|
"example.co.uk",
|
||||||
|
"example.com",
|
||||||
|
"exampleapp.com",
|
||||||
|
]);
|
||||||
|
domainSettingsService.equivalentDomains$ = of([["not-example.com"]]);
|
||||||
const pageUrl = "https://subdomain.example.com";
|
const pageUrl = "https://subdomain.example.com";
|
||||||
const tabUrl = "https://www.not-example.com";
|
const tabUrl = "https://www.not-example.com";
|
||||||
const equivalentDomains = new Set(["not-example.com"]);
|
|
||||||
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
|
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
|
||||||
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
|
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
|
||||||
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
|
|
||||||
|
|
||||||
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
|
const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
|
||||||
|
|
||||||
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
|
|
||||||
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
|
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
|
||||||
pageUrl,
|
pageUrl,
|
||||||
equivalentDomains,
|
equivalentDomains,
|
||||||
|
@ -1,15 +1,19 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
|
import {
|
||||||
|
UriMatchStrategySetting,
|
||||||
|
UriMatchStrategy,
|
||||||
|
} from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
|
||||||
import { FieldType, UriMatchType, CipherType } from "@bitwarden/common/vault/enums";
|
import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
|
||||||
@ -48,7 +52,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
private totpService: TotpService,
|
private totpService: TotpService,
|
||||||
private eventCollectionService: EventCollectionService,
|
private eventCollectionService: EventCollectionService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private settingsService: SettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private userVerificationService: UserVerificationService,
|
private userVerificationService: UserVerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -215,6 +219,13 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$);
|
return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the default URI match strategy setting from the domain settings service.
|
||||||
|
*/
|
||||||
|
async getDefaultUriMatchStrategy(): Promise<UriMatchStrategySetting> {
|
||||||
|
return await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Autofill a given tab with a given login item
|
* Autofill a given tab with a given login item
|
||||||
* @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item
|
* @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item
|
||||||
@ -229,7 +240,7 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
let totp: string | null = null;
|
let totp: string | null = null;
|
||||||
|
|
||||||
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
const canAccessPremium = await this.stateService.getCanAccessPremium();
|
||||||
const defaultUriMatch = (await this.stateService.getDefaultUriMatch()) ?? UriMatchType.Domain;
|
const defaultUriMatch = await this.getDefaultUriMatchStrategy();
|
||||||
|
|
||||||
if (!canAccessPremium) {
|
if (!canAccessPremium) {
|
||||||
options.cipher.login.totp = null;
|
options.cipher.login.totp = null;
|
||||||
@ -579,9 +590,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
let totp: AutofillField = null;
|
let totp: AutofillField = null;
|
||||||
const login = options.cipher.login;
|
const login = options.cipher.login;
|
||||||
fillScript.savedUrls =
|
fillScript.savedUrls =
|
||||||
login?.uris?.filter((u) => u.match != UriMatchType.Never).map((u) => u.uri) ?? [];
|
login?.uris?.filter((u) => u.match != UriMatchStrategy.Never).map((u) => u.uri) ?? [];
|
||||||
|
|
||||||
fillScript.untrustedIframe = this.inUntrustedIframe(pageDetails.url, options);
|
fillScript.untrustedIframe = await this.inUntrustedIframe(pageDetails.url, options);
|
||||||
|
|
||||||
let passwordFields = AutofillService.loadPasswordFields(
|
let passwordFields = AutofillService.loadPasswordFields(
|
||||||
pageDetails,
|
pageDetails,
|
||||||
@ -1066,7 +1077,10 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
* @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
|
* @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private inUntrustedIframe(pageUrl: string, options: GenerateFillScriptOptions): boolean {
|
private async inUntrustedIframe(
|
||||||
|
pageUrl: string,
|
||||||
|
options: GenerateFillScriptOptions,
|
||||||
|
): Promise<boolean> {
|
||||||
// If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe
|
// If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe
|
||||||
// This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway
|
// This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway
|
||||||
if (pageUrl === options.tabUrl) {
|
if (pageUrl === options.tabUrl) {
|
||||||
@ -1076,7 +1090,9 @@ export default class AutofillService implements AutofillServiceInterface {
|
|||||||
// Check the pageUrl against cipher URIs using the configured match detection.
|
// Check the pageUrl against cipher URIs using the configured match detection.
|
||||||
// Remember: if we are in this function, the tabUrl already matches a saved URI for the login.
|
// Remember: if we are in this function, the tabUrl already matches a saved URI for the login.
|
||||||
// We need to verify the pageUrl also matches.
|
// We need to verify the pageUrl also matches.
|
||||||
const equivalentDomains = this.settingsService.getEquivalentDomains(pageUrl);
|
const equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(pageUrl),
|
||||||
|
);
|
||||||
const matchesUri = options.cipher.login.matchesUri(
|
const matchesUri = options.cipher.login.matchesUri(
|
||||||
pageUrl,
|
pageUrl,
|
||||||
equivalentDomains,
|
equivalentDomains,
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
|
||||||
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
|
||||||
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
|
|
||||||
@ -112,7 +113,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr
|
|||||||
allowTotpAutofill: false,
|
allowTotpAutofill: false,
|
||||||
cipher: mock<CipherView>(),
|
cipher: mock<CipherView>(),
|
||||||
tabUrl: "https://jest-testing-website.com",
|
tabUrl: "https://jest-testing-website.com",
|
||||||
defaultUriMatch: UriMatchType.Domain,
|
defaultUriMatch: UriMatchStrategy.Domain,
|
||||||
...customFields,
|
...customFields,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
|
import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
|
|
||||||
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
|
||||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||||
|
|
||||||
@ -32,15 +31,10 @@ export type UserSettings = {
|
|||||||
utcDate: string;
|
utcDate: string;
|
||||||
version: string;
|
version: string;
|
||||||
};
|
};
|
||||||
settings: {
|
|
||||||
equivalentDomains: string[][];
|
|
||||||
};
|
|
||||||
vaultTimeout: number;
|
vaultTimeout: number;
|
||||||
vaultTimeoutAction: VaultTimeoutAction;
|
vaultTimeoutAction: VaultTimeoutAction;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GlobalSettings = Pick<GlobalState, "neverDomains">;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A HTMLElement (usually a form element) with additional custom properties added by this script
|
* A HTMLElement (usually a form element) with additional custom properties added by this script
|
||||||
*/
|
*/
|
||||||
|
@ -56,6 +56,10 @@ import {
|
|||||||
BadgeSettingsServiceAbstraction,
|
BadgeSettingsServiceAbstraction,
|
||||||
BadgeSettingsService,
|
BadgeSettingsService,
|
||||||
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
import {
|
||||||
|
DomainSettingsService,
|
||||||
|
DefaultDomainSettingsService,
|
||||||
|
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import {
|
import {
|
||||||
UserNotificationSettingsService,
|
UserNotificationSettingsService,
|
||||||
UserNotificationSettingsServiceAbstraction,
|
UserNotificationSettingsServiceAbstraction,
|
||||||
@ -198,10 +202,11 @@ import { BrowserEnvironmentService } from "../platform/services/browser-environm
|
|||||||
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
import BrowserLocalStorageService from "../platform/services/browser-local-storage.service";
|
||||||
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
|
import BrowserMessagingPrivateModeBackgroundService from "../platform/services/browser-messaging-private-mode-background.service";
|
||||||
import BrowserMessagingService from "../platform/services/browser-messaging.service";
|
import BrowserMessagingService from "../platform/services/browser-messaging.service";
|
||||||
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
|
||||||
import { BrowserStateService } from "../platform/services/browser-state.service";
|
import { BrowserStateService } from "../platform/services/browser-state.service";
|
||||||
import I18nService from "../platform/services/i18n.service";
|
import I18nService from "../platform/services/i18n.service";
|
||||||
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
import { LocalBackedSessionStorageService } from "../platform/services/local-backed-session-storage.service";
|
||||||
|
import { BackgroundPlatformUtilsService } from "../platform/services/platform-utils/background-platform-utils.service";
|
||||||
|
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||||
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
|
import { BackgroundDerivedStateProvider } from "../platform/state/background-derived-state.provider";
|
||||||
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
import { BackgroundMemoryStorageService } from "../platform/storage/background-memory-storage.service";
|
||||||
import { BrowserSendService } from "../services/browser-send.service";
|
import { BrowserSendService } from "../services/browser-send.service";
|
||||||
@ -258,6 +263,7 @@ export default class MainBackground {
|
|||||||
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
|
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
|
||||||
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
||||||
badgeSettingsService: BadgeSettingsServiceAbstraction;
|
badgeSettingsService: BadgeSettingsServiceAbstraction;
|
||||||
|
domainSettingsService: DomainSettingsService;
|
||||||
systemService: SystemServiceAbstraction;
|
systemService: SystemServiceAbstraction;
|
||||||
eventCollectionService: EventCollectionServiceAbstraction;
|
eventCollectionService: EventCollectionServiceAbstraction;
|
||||||
eventUploadService: EventUploadServiceAbstraction;
|
eventUploadService: EventUploadServiceAbstraction;
|
||||||
@ -438,28 +444,10 @@ export default class MainBackground {
|
|||||||
migrationRunner,
|
migrationRunner,
|
||||||
);
|
);
|
||||||
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
|
this.userNotificationSettingsService = new UserNotificationSettingsService(this.stateProvider);
|
||||||
this.platformUtilsService = new BrowserPlatformUtilsService(
|
this.platformUtilsService = new BackgroundPlatformUtilsService(
|
||||||
this.messagingService,
|
this.messagingService,
|
||||||
(clipboardValue, clearMs) => {
|
(clipboardValue, clearMs) => this.clearClipboard(clipboardValue, clearMs),
|
||||||
if (this.systemService != null) {
|
async () => this.biometricUnlock(),
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.systemService.clearClipboard(clipboardValue, clearMs);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
if (this.nativeMessagingBackground != null) {
|
|
||||||
const promise = this.nativeMessagingBackground.getResponse();
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
|
|
||||||
} catch (e) {
|
|
||||||
return Promise.reject(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return promise.then((result) => result.response === "unlocked");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
self,
|
self,
|
||||||
);
|
);
|
||||||
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
this.i18nService = new I18nService(BrowserApi.getUILanguage(), this.globalStateProvider);
|
||||||
@ -475,7 +463,7 @@ export default class MainBackground {
|
|||||||
this.biometricStateService,
|
this.biometricStateService,
|
||||||
);
|
);
|
||||||
this.tokenService = new TokenService(this.stateService);
|
this.tokenService = new TokenService(this.stateService);
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
this.appIdService = new AppIdService(this.globalStateProvider);
|
||||||
this.apiService = new ApiService(
|
this.apiService = new ApiService(
|
||||||
this.tokenService,
|
this.tokenService,
|
||||||
this.platformUtilsService,
|
this.platformUtilsService,
|
||||||
@ -483,6 +471,7 @@ export default class MainBackground {
|
|||||||
this.appIdService,
|
this.appIdService,
|
||||||
(expired: boolean) => this.logout(expired),
|
(expired: boolean) => this.logout(expired),
|
||||||
);
|
);
|
||||||
|
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
|
||||||
this.settingsService = new BrowserSettingsService(this.stateService);
|
this.settingsService = new BrowserSettingsService(this.stateService);
|
||||||
this.fileUploadService = new FileUploadService(this.logService);
|
this.fileUploadService = new FileUploadService(this.logService);
|
||||||
this.cipherFileUploadService = new CipherFileUploadService(
|
this.cipherFileUploadService = new CipherFileUploadService(
|
||||||
@ -588,6 +577,7 @@ export default class MainBackground {
|
|||||||
this.policyService,
|
this.policyService,
|
||||||
this.deviceTrustCryptoService,
|
this.deviceTrustCryptoService,
|
||||||
this.authRequestService,
|
this.authRequestService,
|
||||||
|
this.globalStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
this.ssoLoginService = new SsoLoginService(this.stateProvider);
|
||||||
@ -607,7 +597,7 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.cipherService = new CipherService(
|
this.cipherService = new CipherService(
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.settingsService,
|
this.domainSettingsService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.searchService,
|
this.searchService,
|
||||||
@ -695,7 +685,7 @@ export default class MainBackground {
|
|||||||
this.providerService = new ProviderService(this.stateProvider);
|
this.providerService = new ProviderService(this.stateProvider);
|
||||||
this.syncService = new SyncService(
|
this.syncService = new SyncService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.settingsService,
|
this.domainSettingsService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
@ -732,7 +722,7 @@ export default class MainBackground {
|
|||||||
this.totpService,
|
this.totpService,
|
||||||
this.eventCollectionService,
|
this.eventCollectionService,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.settingsService,
|
this.domainSettingsService,
|
||||||
this.userVerificationService,
|
this.userVerificationService,
|
||||||
);
|
);
|
||||||
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
|
||||||
@ -796,6 +786,7 @@ export default class MainBackground {
|
|||||||
this.authService,
|
this.authService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.vaultSettingsService,
|
this.vaultSettingsService,
|
||||||
|
this.domainSettingsService,
|
||||||
this.logService,
|
this.logService,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -865,6 +856,7 @@ export default class MainBackground {
|
|||||||
this.folderService,
|
this.folderService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.userNotificationSettingsService,
|
this.userNotificationSettingsService,
|
||||||
|
this.domainSettingsService,
|
||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.logService,
|
this.logService,
|
||||||
);
|
);
|
||||||
@ -1098,7 +1090,6 @@ export default class MainBackground {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.syncService.setLastSync(new Date(0), userId),
|
this.syncService.setLastSync(new Date(0), userId),
|
||||||
this.cryptoService.clearKeys(userId),
|
this.cryptoService.clearKeys(userId),
|
||||||
this.settingsService.clear(userId),
|
|
||||||
this.cipherService.clear(userId),
|
this.cipherService.clear(userId),
|
||||||
this.folderService.clear(userId),
|
this.folderService.clear(userId),
|
||||||
this.collectionService.clear(userId),
|
this.collectionService.clear(userId),
|
||||||
@ -1223,6 +1214,23 @@ export default class MainBackground {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async clearClipboard(clipboardValue: string, clearMs: number) {
|
||||||
|
if (this.systemService != null) {
|
||||||
|
await this.systemService.clearClipboard(clipboardValue, clearMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async biometricUnlock(): Promise<boolean> {
|
||||||
|
if (this.nativeMessagingBackground == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsePromise = this.nativeMessagingBackground.getResponse();
|
||||||
|
await this.nativeMessagingBackground.send({ command: "biometricUnlock" });
|
||||||
|
const response = await responsePromise;
|
||||||
|
return response.response === "unlocked";
|
||||||
|
}
|
||||||
|
|
||||||
private async fullSync(override = false) {
|
private async fullSync(override = false) {
|
||||||
const syncInternal = 6 * 60 * 60 * 1000; // 6 hours
|
const syncInternal = 6 * 60 * 60 * 1000; // 6 hours
|
||||||
const lastSync = await this.syncService.getLastSync();
|
const lastSync = await this.syncService.getLastSync();
|
||||||
|
@ -122,7 +122,7 @@ export class NativeMessagingBackground {
|
|||||||
break;
|
break;
|
||||||
case "disconnected":
|
case "disconnected":
|
||||||
if (this.connecting) {
|
if (this.connecting) {
|
||||||
reject("startDesktop");
|
reject(new Error("startDesktop"));
|
||||||
}
|
}
|
||||||
this.connected = false;
|
this.connected = false;
|
||||||
this.port.disconnect();
|
this.port.disconnect();
|
||||||
@ -203,7 +203,7 @@ export class NativeMessagingBackground {
|
|||||||
this.connected = false;
|
this.connected = false;
|
||||||
|
|
||||||
const reason = error != null ? "desktopIntegrationDisabled" : null;
|
const reason = error != null ? "desktopIntegrationDisabled" : null;
|
||||||
reject(reason);
|
reject(new Error(reason));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import { AutofillService } from "../autofill/services/abstractions/autofill.serv
|
|||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
import { BrowserApi } from "../platform/browser/browser-api";
|
||||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
||||||
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
import { BrowserEnvironmentService } from "../platform/services/browser-environment.service";
|
||||||
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||||
import { AbortManager } from "../vault/background/abort-manager";
|
import { AbortManager } from "../vault/background/abort-manager";
|
||||||
import { Fido2Service } from "../vault/services/abstractions/fido2.service";
|
import { Fido2Service } from "../vault/services/abstractions/fido2.service";
|
||||||
|
|
||||||
@ -68,6 +68,7 @@ export default class RuntimeBackground {
|
|||||||
"checkFido2FeatureEnabled",
|
"checkFido2FeatureEnabled",
|
||||||
"fido2RegisterCredentialRequest",
|
"fido2RegisterCredentialRequest",
|
||||||
"fido2GetCredentialRequest",
|
"fido2GetCredentialRequest",
|
||||||
|
"biometricUnlock",
|
||||||
];
|
];
|
||||||
|
|
||||||
if (messagesWithResponse.includes(msg.command)) {
|
if (messagesWithResponse.includes(msg.command)) {
|
||||||
@ -305,6 +306,14 @@ export default class RuntimeBackground {
|
|||||||
);
|
);
|
||||||
case "switchAccount": {
|
case "switchAccount": {
|
||||||
await this.main.switchAccount(msg.userId);
|
await this.main.switchAccount(msg.userId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "clearClipboard": {
|
||||||
|
await this.main.clearClipboard(msg.clipboardValue, msg.timeoutMs);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "biometricUnlock": {
|
||||||
|
return await this.main.biometricUnlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import { DiskStorageOptions } from "@koa/multer";
|
|
||||||
|
|
||||||
import { AppIdService as AbstractAppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService as AbstractAppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/services/app-id.service";
|
||||||
|
|
||||||
import { FactoryOptions, CachedServices, factory } from "./factory-options";
|
import { FactoryOptions, CachedServices, factory } from "./factory-options";
|
||||||
import { diskStorageServiceFactory } from "./storage-service.factory";
|
import {
|
||||||
|
GlobalStateProviderInitOptions,
|
||||||
|
globalStateProviderFactory,
|
||||||
|
} from "./global-state-provider.factory";
|
||||||
|
|
||||||
type AppIdServiceFactoryOptions = FactoryOptions;
|
type AppIdServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
export type AppIdServiceInitOptions = AppIdServiceFactoryOptions & DiskStorageOptions;
|
export type AppIdServiceInitOptions = AppIdServiceFactoryOptions & GlobalStateProviderInitOptions;
|
||||||
|
|
||||||
export function appIdServiceFactory(
|
export function appIdServiceFactory(
|
||||||
cache: { appIdService?: AbstractAppIdService } & CachedServices,
|
cache: { appIdService?: AbstractAppIdService } & CachedServices,
|
||||||
@ -18,6 +19,6 @@ export function appIdServiceFactory(
|
|||||||
cache,
|
cache,
|
||||||
"appIdService",
|
"appIdService",
|
||||||
opts,
|
opts,
|
||||||
async () => new AppIdService(await diskStorageServiceFactory(cache, opts)),
|
async () => new AppIdService(await globalStateProviderFactory(cache, opts)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
import BrowserPlatformUtilsService from "../../services/browser-platform-utils.service";
|
import { BackgroundPlatformUtilsService } from "../../services/platform-utils/background-platform-utils.service";
|
||||||
|
|
||||||
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
import { CachedServices, factory, FactoryOptions } from "./factory-options";
|
||||||
import { MessagingServiceInitOptions, messagingServiceFactory } from "./messaging-service.factory";
|
import { MessagingServiceInitOptions, messagingServiceFactory } from "./messaging-service.factory";
|
||||||
@ -25,7 +25,7 @@ export function platformUtilsServiceFactory(
|
|||||||
"platformUtilsService",
|
"platformUtilsService",
|
||||||
opts,
|
opts,
|
||||||
async () =>
|
async () =>
|
||||||
new BrowserPlatformUtilsService(
|
new BackgroundPlatformUtilsService(
|
||||||
await messagingServiceFactory(cache, opts),
|
await messagingServiceFactory(cache, opts),
|
||||||
opts.platformUtilsServiceOptions.clipboardWriteCallback,
|
opts.platformUtilsServiceOptions.clipboardWriteCallback,
|
||||||
opts.platformUtilsServiceOptions.biometricCallback,
|
opts.platformUtilsServiceOptions.biometricCallback,
|
||||||
|
@ -3,7 +3,7 @@ import { Observable } from "rxjs";
|
|||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
|
||||||
import { TabMessage } from "../../types/tab-messages";
|
import { TabMessage } from "../../types/tab-messages";
|
||||||
import BrowserPlatformUtilsService from "../services/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
|
||||||
|
|
||||||
export class BrowserApi {
|
export class BrowserApi {
|
||||||
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
static isWebExtensionsApi: boolean = typeof browser !== "undefined";
|
||||||
|
@ -17,7 +17,7 @@ import { Account } from "../../models/account";
|
|||||||
import IconDetails from "../../vault/background/models/icon-details";
|
import IconDetails from "../../vault/background/models/icon-details";
|
||||||
import { cipherServiceFactory } from "../../vault/background/service_factories/cipher-service.factory";
|
import { cipherServiceFactory } from "../../vault/background/service_factories/cipher-service.factory";
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../browser/browser-api";
|
||||||
import BrowserPlatformUtilsService from "../services/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../services/platform-utils/browser-platform-utils.service";
|
||||||
|
|
||||||
export type BadgeOptions = {
|
export type BadgeOptions = {
|
||||||
tab?: chrome.tabs.Tab;
|
tab?: chrome.tabs.Tab;
|
||||||
|
@ -0,0 +1,28 @@
|
|||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
|
||||||
|
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||||
|
|
||||||
|
export class BackgroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||||
|
constructor(
|
||||||
|
private messagingService: MessagingService,
|
||||||
|
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||||
|
biometricCallback: () => Promise<boolean>,
|
||||||
|
win: Window & typeof globalThis,
|
||||||
|
) {
|
||||||
|
super(clipboardWriteCallback, biometricCallback, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
override showToast(
|
||||||
|
type: "error" | "success" | "warning" | "info",
|
||||||
|
title: string,
|
||||||
|
text: string | string[],
|
||||||
|
options?: any,
|
||||||
|
): void {
|
||||||
|
this.messagingService.send("showToast", {
|
||||||
|
text: text,
|
||||||
|
title: title,
|
||||||
|
type: type,
|
||||||
|
options: options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,26 @@
|
|||||||
import { DeviceType } from "@bitwarden/common/enums";
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
|
||||||
import { flushPromises } from "../../autofill/spec/testing-utils";
|
import { flushPromises } from "../../../autofill/spec/testing-utils";
|
||||||
import { SafariApp } from "../../browser/safariApp";
|
import { SafariApp } from "../../../browser/safariApp";
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../../browser/browser-api";
|
||||||
|
import BrowserClipboardService from "../browser-clipboard.service";
|
||||||
|
|
||||||
import BrowserClipboardService from "./browser-clipboard.service";
|
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||||
import BrowserPlatformUtilsService from "./browser-platform-utils.service";
|
|
||||||
|
class TestBrowserPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||||
|
constructor(clipboardSpy: jest.Mock, win: Window & typeof globalThis) {
|
||||||
|
super(clipboardSpy, null, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(
|
||||||
|
type: "error" | "success" | "warning" | "info",
|
||||||
|
title: string,
|
||||||
|
text: string | string[],
|
||||||
|
options?: any,
|
||||||
|
): void {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe("Browser Utils Service", () => {
|
describe("Browser Utils Service", () => {
|
||||||
let browserPlatformUtilsService: BrowserPlatformUtilsService;
|
let browserPlatformUtilsService: BrowserPlatformUtilsService;
|
||||||
@ -13,10 +28,8 @@ describe("Browser Utils Service", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
|
(window as any).matchMedia = jest.fn().mockReturnValueOnce({});
|
||||||
browserPlatformUtilsService = new BrowserPlatformUtilsService(
|
browserPlatformUtilsService = new TestBrowserPlatformUtilsService(
|
||||||
null,
|
|
||||||
clipboardWriteCallbackSpy,
|
clipboardWriteCallbackSpy,
|
||||||
null,
|
|
||||||
window,
|
window,
|
||||||
);
|
);
|
||||||
});
|
});
|
@ -1,20 +1,17 @@
|
|||||||
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
import { ClientType, DeviceType } from "@bitwarden/common/enums";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
import {
|
import {
|
||||||
ClipboardOptions,
|
ClipboardOptions,
|
||||||
PlatformUtilsService,
|
PlatformUtilsService,
|
||||||
} from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
} from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
import { SafariApp } from "../../browser/safariApp";
|
import { SafariApp } from "../../../browser/safariApp";
|
||||||
import { BrowserApi } from "../browser/browser-api";
|
import { BrowserApi } from "../../browser/browser-api";
|
||||||
|
import BrowserClipboardService from "../browser-clipboard.service";
|
||||||
|
|
||||||
import BrowserClipboardService from "./browser-clipboard.service";
|
export abstract class BrowserPlatformUtilsService implements PlatformUtilsService {
|
||||||
|
|
||||||
export default class BrowserPlatformUtilsService implements PlatformUtilsService {
|
|
||||||
private static deviceCache: DeviceType = null;
|
private static deviceCache: DeviceType = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private messagingService: MessagingService,
|
|
||||||
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
private clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||||
private biometricCallback: () => Promise<boolean>,
|
private biometricCallback: () => Promise<boolean>,
|
||||||
private globalContext: Window | ServiceWorkerGlobalScope,
|
private globalContext: Window | ServiceWorkerGlobalScope,
|
||||||
@ -193,19 +190,12 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
showToast(
|
abstract showToast(
|
||||||
type: "error" | "success" | "warning" | "info",
|
type: "error" | "success" | "warning" | "info",
|
||||||
title: string,
|
title: string,
|
||||||
text: string | string[],
|
text: string | string[],
|
||||||
options?: any,
|
options?: any,
|
||||||
): void {
|
): void;
|
||||||
this.messagingService.send("showToast", {
|
|
||||||
text: text,
|
|
||||||
title: title,
|
|
||||||
type: type,
|
|
||||||
options: options,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
isDev(): boolean {
|
isDev(): boolean {
|
||||||
return process.env.ENV === "development";
|
return process.env.ENV === "development";
|
||||||
@ -279,11 +269,10 @@ export default class BrowserPlatformUtilsService implements PlatformUtilsService
|
|||||||
|
|
||||||
async supportsBiometric() {
|
async supportsBiometric() {
|
||||||
const platformInfo = await BrowserApi.getPlatformInfo();
|
const platformInfo = await BrowserApi.getPlatformInfo();
|
||||||
if (platformInfo.os === "android") {
|
if (platformInfo.os === "mac" || platformInfo.os === "win") {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticateBiometric() {
|
authenticateBiometric() {
|
@ -0,0 +1,40 @@
|
|||||||
|
import { SecurityContext } from "@angular/core";
|
||||||
|
import { DomSanitizer } from "@angular/platform-browser";
|
||||||
|
import { ToastrService } from "ngx-toastr";
|
||||||
|
|
||||||
|
import { BrowserPlatformUtilsService } from "./browser-platform-utils.service";
|
||||||
|
|
||||||
|
export class ForegroundPlatformUtilsService extends BrowserPlatformUtilsService {
|
||||||
|
constructor(
|
||||||
|
private sanitizer: DomSanitizer,
|
||||||
|
private toastrService: ToastrService,
|
||||||
|
clipboardWriteCallback: (clipboardValue: string, clearMs: number) => void,
|
||||||
|
biometricCallback: () => Promise<boolean>,
|
||||||
|
win: Window & typeof globalThis,
|
||||||
|
) {
|
||||||
|
super(clipboardWriteCallback, biometricCallback, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
override showToast(
|
||||||
|
type: "error" | "success" | "warning" | "info",
|
||||||
|
title: string,
|
||||||
|
text: string | string[],
|
||||||
|
options?: any,
|
||||||
|
): void {
|
||||||
|
if (typeof text === "string") {
|
||||||
|
// Already in the correct format
|
||||||
|
} else if (text.length === 1) {
|
||||||
|
text = text[0];
|
||||||
|
} else {
|
||||||
|
let message = "";
|
||||||
|
text.forEach(
|
||||||
|
(t: string) =>
|
||||||
|
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"),
|
||||||
|
);
|
||||||
|
text = message;
|
||||||
|
options.enableHtml = true;
|
||||||
|
}
|
||||||
|
this.toastrService.show(text, title, options, "toast-" + type);
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +1,18 @@
|
|||||||
import {
|
import { ChangeDetectorRef, Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
NgZone,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
SecurityContext,
|
|
||||||
} from "@angular/core";
|
|
||||||
import { DomSanitizer } from "@angular/platform-browser";
|
|
||||||
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
import { NavigationEnd, Router, RouterOutlet } from "@angular/router";
|
||||||
import { IndividualConfig, ToastrService } from "ngx-toastr";
|
import { ToastrService } from "ngx-toastr";
|
||||||
import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs";
|
import { filter, concatMap, Subject, takeUntil, firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
import { DialogService, SimpleDialogOptions } from "@bitwarden/components";
|
||||||
|
|
||||||
import { BrowserApi } from "../platform/browser/browser-api";
|
import { BrowserApi } from "../platform/browser/browser-api";
|
||||||
import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service";
|
import { ZonedMessageListenerService } from "../platform/browser/zoned-message-listener.service";
|
||||||
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
import { BrowserStateService } from "../platform/services/abstractions/browser-state.service";
|
||||||
|
import { ForegroundPlatformUtilsService } from "../platform/services/platform-utils/foreground-platform-utils.service";
|
||||||
|
|
||||||
import { routerTransition } from "./app-routing.animations";
|
import { routerTransition } from "./app-routing.animations";
|
||||||
import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component";
|
import { DesktopSyncVerificationDialogComponent } from "./components/desktop-sync-verification-dialog.component";
|
||||||
@ -48,11 +39,9 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private stateService: BrowserStateService,
|
private stateService: BrowserStateService,
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private messagingService: MessagingService,
|
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private ngZone: NgZone,
|
private ngZone: NgZone,
|
||||||
private sanitizer: DomSanitizer,
|
private platformUtilsService: ForegroundPlatformUtilsService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
private browserMessagingApi: ZonedMessageListenerService,
|
private browserMessagingApi: ZonedMessageListenerService,
|
||||||
) {}
|
) {}
|
||||||
@ -219,31 +208,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private showToast(msg: any) {
|
private showToast(msg: any) {
|
||||||
let message = "";
|
this.platformUtilsService.showToast(msg.type, msg.title, msg.text, msg.options);
|
||||||
|
|
||||||
const options: Partial<IndividualConfig> = {};
|
|
||||||
|
|
||||||
if (typeof msg.text === "string") {
|
|
||||||
message = msg.text;
|
|
||||||
} else if (msg.text.length === 1) {
|
|
||||||
message = msg.text[0];
|
|
||||||
} else {
|
|
||||||
msg.text.forEach(
|
|
||||||
(t: string) =>
|
|
||||||
(message += "<p>" + this.sanitizer.sanitize(SecurityContext.HTML, t) + "</p>"),
|
|
||||||
);
|
|
||||||
options.enableHtml = true;
|
|
||||||
}
|
|
||||||
if (msg.options != null) {
|
|
||||||
if (msg.options.trustedHtml === true) {
|
|
||||||
options.enableHtml = true;
|
|
||||||
}
|
|
||||||
if (msg.options.timeout != null && msg.options.timeout > 0) {
|
|
||||||
options.timeOut = msg.options.timeout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toastrService.show(message, msg.title, options, "toast-" + msg.type);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async showDialog(msg: SimpleDialogOptions) {
|
private async showDialog(msg: SimpleDialogOptions) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { enableProdMode } from "@angular/core";
|
import { enableProdMode } from "@angular/core";
|
||||||
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
import { platformBrowserDynamic } from "@angular/platform-browser-dynamic";
|
||||||
|
|
||||||
import BrowserPlatformUtilsService from "../platform/services/browser-platform-utils.service";
|
import { BrowserPlatformUtilsService } from "../platform/services/platform-utils/browser-platform-utils.service";
|
||||||
|
|
||||||
require("./scss/popup.scss");
|
require("./scss/popup.scss");
|
||||||
require("./scss/tailwind.css");
|
require("./scss/tailwind.css");
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
import { APP_INITIALIZER, NgModule, NgZone } from "@angular/core";
|
||||||
|
import { DomSanitizer } from "@angular/platform-browser";
|
||||||
|
import { ToastrService } from "ngx-toastr";
|
||||||
|
|
||||||
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
|
import { UnauthGuard as BaseUnauthGuardService } from "@bitwarden/angular/auth/guards";
|
||||||
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
|
import { ThemingService } from "@bitwarden/angular/platform/services/theming/theming.service";
|
||||||
@ -44,7 +46,6 @@ import {
|
|||||||
UserNotificationSettingsService,
|
UserNotificationSettingsService,
|
||||||
UserNotificationSettingsServiceAbstraction,
|
UserNotificationSettingsServiceAbstraction,
|
||||||
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
} from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
@ -118,6 +119,7 @@ import BrowserMessagingPrivateModePopupService from "../../platform/services/bro
|
|||||||
import BrowserMessagingService from "../../platform/services/browser-messaging.service";
|
import BrowserMessagingService from "../../platform/services/browser-messaging.service";
|
||||||
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
import { BrowserStateService } from "../../platform/services/browser-state.service";
|
||||||
import I18nService from "../../platform/services/i18n.service";
|
import I18nService from "../../platform/services/i18n.service";
|
||||||
|
import { ForegroundPlatformUtilsService } from "../../platform/services/platform-utils/foreground-platform-utils.service";
|
||||||
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
|
import { ForegroundDerivedStateProvider } from "../../platform/state/foreground-derived-state.provider";
|
||||||
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service";
|
||||||
import { BrowserSendService } from "../../services/browser-send.service";
|
import { BrowserSendService } from "../../services/browser-send.service";
|
||||||
@ -291,8 +293,32 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: PlatformUtilsService,
|
provide: PlatformUtilsService,
|
||||||
useFactory: getBgService<PlatformUtilsService>("platformUtilsService"),
|
useExisting: ForegroundPlatformUtilsService,
|
||||||
deps: [],
|
},
|
||||||
|
{
|
||||||
|
provide: ForegroundPlatformUtilsService,
|
||||||
|
useClass: ForegroundPlatformUtilsService,
|
||||||
|
useFactory: (sanitizer: DomSanitizer, toastrService: ToastrService) => {
|
||||||
|
return new ForegroundPlatformUtilsService(
|
||||||
|
sanitizer,
|
||||||
|
toastrService,
|
||||||
|
(clipboardValue: string, clearMs: number) => {
|
||||||
|
void BrowserApi.sendMessage("clearClipboard", { clipboardValue, clearMs });
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
const response = await BrowserApi.sendMessageWithResponse<{
|
||||||
|
result: boolean;
|
||||||
|
error: string;
|
||||||
|
}>("biometricUnlock");
|
||||||
|
if (!response.result) {
|
||||||
|
throw response.error;
|
||||||
|
}
|
||||||
|
return response.result;
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
deps: [DomSanitizer, ToastrService],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: PasswordStrengthServiceAbstraction,
|
provide: PasswordStrengthServiceAbstraction,
|
||||||
@ -349,7 +375,6 @@ function getBgService<T>(service: keyof MainBackground) {
|
|||||||
useClass: BrowserLocalStorageService,
|
useClass: BrowserLocalStorageService,
|
||||||
deps: [],
|
deps: [],
|
||||||
},
|
},
|
||||||
{ provide: AppIdService, useFactory: getBgService<AppIdService>("appIdService"), deps: [] },
|
|
||||||
{
|
{
|
||||||
provide: AutofillService,
|
provide: AutofillService,
|
||||||
useFactory: getBgService<AutofillService>("autofillService"),
|
useFactory: getBgService<AutofillService>("autofillService"),
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@ -30,6 +32,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private broadcasterService: BroadcasterService,
|
private broadcasterService: BroadcasterService,
|
||||||
@ -40,7 +43,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const savedDomains = await this.stateService.getNeverDomains();
|
const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||||
if (savedDomains) {
|
if (savedDomains) {
|
||||||
for (const uri of Object.keys(savedDomains)) {
|
for (const uri of Object.keys(savedDomains)) {
|
||||||
this.excludedDomains.push({ uri: uri, showCurrentUris: false });
|
this.excludedDomains.push({ uri: uri, showCurrentUris: false });
|
||||||
@ -107,7 +110,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.stateService.setNeverDomains(savedDomains);
|
await this.domainSettingsService.setNeverDomains(savedDomains);
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate(["/tabs/settings"]);
|
this.router.navigate(["/tabs/settings"]);
|
||||||
|
@ -5,14 +5,18 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
|
|||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
|
||||||
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
|
||||||
|
import {
|
||||||
|
UriMatchStrategy,
|
||||||
|
UriMatchStrategySetting,
|
||||||
|
} from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { ThemeType } from "@bitwarden/common/platform/enums";
|
import { ThemeType } from "@bitwarden/common/platform/enums";
|
||||||
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
|
||||||
import { UriMatchType } from "@bitwarden/common/vault/enums";
|
|
||||||
|
|
||||||
import { enableAccountSwitching } from "../../platform/flags";
|
import { enableAccountSwitching } from "../../platform/flags";
|
||||||
|
|
||||||
@ -36,7 +40,7 @@ export class OptionsComponent implements OnInit {
|
|||||||
showClearClipboard = true;
|
showClearClipboard = true;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
themeOptions: any[];
|
themeOptions: any[];
|
||||||
defaultUriMatch = UriMatchType.Domain;
|
defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
|
||||||
uriMatchOptions: any[];
|
uriMatchOptions: any[];
|
||||||
clearClipboard: ClearClipboardDelaySetting;
|
clearClipboard: ClearClipboardDelaySetting;
|
||||||
clearClipboardOptions: any[];
|
clearClipboardOptions: any[];
|
||||||
@ -50,6 +54,7 @@ export class OptionsComponent implements OnInit {
|
|||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
|
||||||
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
private autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
private badgeSettingsService: BadgeSettingsServiceAbstraction,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
private themingService: AbstractThemingService,
|
private themingService: AbstractThemingService,
|
||||||
@ -64,12 +69,12 @@ export class OptionsComponent implements OnInit {
|
|||||||
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
|
||||||
];
|
];
|
||||||
this.uriMatchOptions = [
|
this.uriMatchOptions = [
|
||||||
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain },
|
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||||
{ name: i18nService.t("host"), value: UriMatchType.Host },
|
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||||
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith },
|
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
||||||
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression },
|
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
||||||
{ name: i18nService.t("exact"), value: UriMatchType.Exact },
|
{ name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
|
||||||
{ name: i18nService.t("never"), value: UriMatchType.Never },
|
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
|
||||||
];
|
];
|
||||||
this.clearClipboardOptions = [
|
this.clearClipboardOptions = [
|
||||||
{ name: i18nService.t("never"), value: null },
|
{ name: i18nService.t("never"), value: null },
|
||||||
@ -122,8 +127,10 @@ export class OptionsComponent implements OnInit {
|
|||||||
|
|
||||||
this.theme = await this.stateService.getTheme();
|
this.theme = await this.stateService.getTheme();
|
||||||
|
|
||||||
const defaultUriMatch = await this.stateService.getDefaultUriMatch();
|
const defaultUriMatch = await firstValueFrom(
|
||||||
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch;
|
this.domainSettingsService.defaultUriMatchStrategy$,
|
||||||
|
);
|
||||||
|
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
|
||||||
|
|
||||||
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
|
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
|
||||||
}
|
}
|
||||||
@ -182,10 +189,6 @@ export class OptionsComponent implements OnInit {
|
|||||||
await this.themingService.updateConfiguredTheme(this.theme);
|
await this.themingService.updateConfiguredTheme(this.theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
async saveDefaultUriMatch() {
|
|
||||||
await this.stateService.setDefaultUriMatch(this.defaultUriMatch);
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveClearClipboard() {
|
async saveClearClipboard() {
|
||||||
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
|
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
|
||||||
}
|
}
|
||||||
|
@ -397,7 +397,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
// Handle connection errors
|
// Handle connection errors
|
||||||
this.form.controls.biometric.setValue(false);
|
this.form.controls.biometric.setValue(false);
|
||||||
|
|
||||||
const error = BiometricErrors[e as BiometricErrorTypes];
|
const error = BiometricErrors[e.message as BiometricErrorTypes];
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
import { BehaviorSubject } from "rxjs";
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { AccountSettingsSettings } from "@bitwarden/common/platform/models/domain/account";
|
|
||||||
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
import { SettingsService } from "@bitwarden/common/services/settings.service";
|
||||||
|
|
||||||
import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable";
|
import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable";
|
||||||
|
|
||||||
@browserSession
|
@browserSession
|
||||||
export class BrowserSettingsService extends SettingsService {
|
export class BrowserSettingsService extends SettingsService {
|
||||||
@sessionSync({ initializer: (obj: string[][]) => obj })
|
|
||||||
protected _settings: BehaviorSubject<AccountSettingsSettings>;
|
|
||||||
|
|
||||||
@sessionSync({ initializer: (b: boolean) => b })
|
@sessionSync({ initializer: (b: boolean) => b })
|
||||||
protected _disableFavicon: BehaviorSubject<boolean>;
|
protected _disableFavicon: BehaviorSubject<boolean>;
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,10 @@ import {
|
|||||||
AutofillSettingsServiceInitOptions,
|
AutofillSettingsServiceInitOptions,
|
||||||
autofillSettingsServiceFactory,
|
autofillSettingsServiceFactory,
|
||||||
} from "../../../autofill/background/service_factories/autofill-settings-service.factory";
|
} from "../../../autofill/background/service_factories/autofill-settings-service.factory";
|
||||||
|
import {
|
||||||
|
DomainSettingsServiceInitOptions,
|
||||||
|
domainSettingsServiceFactory,
|
||||||
|
} from "../../../autofill/background/service_factories/domain-settings-service.factory";
|
||||||
import {
|
import {
|
||||||
CipherFileUploadServiceInitOptions,
|
CipherFileUploadServiceInitOptions,
|
||||||
cipherFileUploadServiceFactory,
|
cipherFileUploadServiceFactory,
|
||||||
@ -13,10 +17,6 @@ import {
|
|||||||
searchServiceFactory,
|
searchServiceFactory,
|
||||||
SearchServiceInitOptions,
|
SearchServiceInitOptions,
|
||||||
} from "../../../background/service-factories/search-service.factory";
|
} from "../../../background/service-factories/search-service.factory";
|
||||||
import {
|
|
||||||
SettingsServiceInitOptions,
|
|
||||||
settingsServiceFactory,
|
|
||||||
} from "../../../background/service-factories/settings-service.factory";
|
|
||||||
import {
|
import {
|
||||||
apiServiceFactory,
|
apiServiceFactory,
|
||||||
ApiServiceInitOptions,
|
ApiServiceInitOptions,
|
||||||
@ -52,13 +52,13 @@ type CipherServiceFactoryOptions = FactoryOptions;
|
|||||||
|
|
||||||
export type CipherServiceInitOptions = CipherServiceFactoryOptions &
|
export type CipherServiceInitOptions = CipherServiceFactoryOptions &
|
||||||
CryptoServiceInitOptions &
|
CryptoServiceInitOptions &
|
||||||
SettingsServiceInitOptions &
|
|
||||||
ApiServiceInitOptions &
|
ApiServiceInitOptions &
|
||||||
CipherFileUploadServiceInitOptions &
|
CipherFileUploadServiceInitOptions &
|
||||||
I18nServiceInitOptions &
|
I18nServiceInitOptions &
|
||||||
SearchServiceInitOptions &
|
SearchServiceInitOptions &
|
||||||
StateServiceInitOptions &
|
StateServiceInitOptions &
|
||||||
AutofillSettingsServiceInitOptions &
|
AutofillSettingsServiceInitOptions &
|
||||||
|
DomainSettingsServiceInitOptions &
|
||||||
EncryptServiceInitOptions &
|
EncryptServiceInitOptions &
|
||||||
ConfigServiceInitOptions;
|
ConfigServiceInitOptions;
|
||||||
|
|
||||||
@ -73,7 +73,7 @@ export function cipherServiceFactory(
|
|||||||
async () =>
|
async () =>
|
||||||
new CipherService(
|
new CipherService(
|
||||||
await cryptoServiceFactory(cache, opts),
|
await cryptoServiceFactory(cache, opts),
|
||||||
await settingsServiceFactory(cache, opts),
|
await domainSettingsServiceFactory(cache, opts),
|
||||||
await apiServiceFactory(cache, opts),
|
await apiServiceFactory(cache, opts),
|
||||||
await i18nServiceFactory(cache, opts),
|
await i18nServiceFactory(cache, opts),
|
||||||
await searchServiceFactory(cache, opts),
|
await searchServiceFactory(cache, opts),
|
||||||
|
@ -3,6 +3,7 @@ import { ConnectedPosition } from "@angular/cdk/overlay";
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
@ -52,6 +53,7 @@ export class Fido2UseBrowserLinkComponent {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
|
private domainSettingsService: DomainSettingsService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
) {}
|
) {}
|
||||||
@ -89,7 +91,7 @@ export class Fido2UseBrowserLinkComponent {
|
|||||||
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
* @param uri - The domain uri to exclude from future FIDO2 prompts.
|
||||||
*/
|
*/
|
||||||
private async handleDomainExclusion(uri: string) {
|
private async handleDomainExclusion(uri: string) {
|
||||||
const exisitingDomains = await this.stateService.getNeverDomains();
|
const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
|
||||||
|
|
||||||
const validDomain = Utils.getHostname(uri);
|
const validDomain = Utils.getHostname(uri);
|
||||||
const savedDomains: { [name: string]: unknown } = {
|
const savedDomains: { [name: string]: unknown } = {
|
||||||
@ -97,9 +99,7 @@ export class Fido2UseBrowserLinkComponent {
|
|||||||
};
|
};
|
||||||
savedDomains[validDomain] = null;
|
savedDomains[validDomain] = null;
|
||||||
|
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
await this.domainSettingsService.setNeverDomains(savedDomains);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
||||||
this.stateService.setNeverDomains(savedDomains);
|
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
combineLatest,
|
combineLatest,
|
||||||
concatMap,
|
concatMap,
|
||||||
filter,
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
map,
|
map,
|
||||||
Observable,
|
Observable,
|
||||||
Subject,
|
Subject,
|
||||||
@ -13,7 +14,7 @@ import {
|
|||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
|
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
@ -72,7 +73,7 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private settingsService: SettingsService,
|
private domainSettingsService: DomainSettingsService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private dialogService: DialogService,
|
private dialogService: DialogService,
|
||||||
@ -133,7 +134,9 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
concatMap(async (message) => {
|
concatMap(async (message) => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case "ConfirmNewCredentialRequest": {
|
case "ConfirmNewCredentialRequest": {
|
||||||
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url);
|
const equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
|
);
|
||||||
|
|
||||||
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
|
||||||
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
|
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
|
||||||
@ -317,7 +320,9 @@ export class Fido2Component implements OnInit, OnDestroy {
|
|||||||
this.ciphers,
|
this.ciphers,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url);
|
const equivalentDomains = await firstValueFrom(
|
||||||
|
this.domainSettingsService.getUrlEquivalentDomains(this.url),
|
||||||
|
);
|
||||||
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
this.displayedCiphers = this.ciphers.filter((cipher) =>
|
||||||
cipher.login.matchesUri(this.url, equivalentDomains),
|
cipher.login.matchesUri(this.url, equivalentDomains),
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
|
||||||
@ -138,9 +137,6 @@ const plugins = [
|
|||||||
entryModule: "src/popup/app.module#AppModule",
|
entryModule: "src/popup/app.module#AppModule",
|
||||||
sourceMap: true,
|
sourceMap: true,
|
||||||
}),
|
}),
|
||||||
new CleanWebpackPlugin({
|
|
||||||
cleanAfterEveryBuildPatterns: ["!popup/fonts/**/*"],
|
|
||||||
}),
|
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
process: "process/browser.js",
|
process: "process/browser.js",
|
||||||
}),
|
}),
|
||||||
@ -244,6 +240,7 @@ const mainConfig = {
|
|||||||
output: {
|
output: {
|
||||||
filename: "[name].js",
|
filename: "[name].js",
|
||||||
path: path.resolve(__dirname, "build"),
|
path: path.resolve(__dirname, "build"),
|
||||||
|
clean: true,
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
noParse: /\.wasm$/,
|
noParse: /\.wasm$/,
|
||||||
|
@ -273,8 +273,8 @@ export class LoginCommand {
|
|||||||
selectedProvider.type === TwoFactorProviderType.Email
|
selectedProvider.type === TwoFactorProviderType.Email
|
||||||
) {
|
) {
|
||||||
const emailReq = new TwoFactorEmailRequest();
|
const emailReq = new TwoFactorEmailRequest();
|
||||||
emailReq.email = this.loginStrategyService.email;
|
emailReq.email = await this.loginStrategyService.getEmail();
|
||||||
emailReq.masterPasswordHash = this.loginStrategyService.masterPasswordHash;
|
emailReq.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||||
await this.apiService.postTwoFactorEmail(emailReq);
|
await this.apiService.postTwoFactorEmail(emailReq);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,10 @@ import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.ser
|
|||||||
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
|
||||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||||
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
|
||||||
|
import {
|
||||||
|
DefaultDomainSettingsService,
|
||||||
|
DomainSettingsService,
|
||||||
|
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { ClientType } from "@bitwarden/common/enums";
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
|
||||||
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
|
||||||
@ -190,6 +194,7 @@ export class Main {
|
|||||||
pinCryptoService: PinCryptoServiceAbstraction;
|
pinCryptoService: PinCryptoServiceAbstraction;
|
||||||
stateService: StateService;
|
stateService: StateService;
|
||||||
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
autofillSettingsService: AutofillSettingsServiceAbstraction;
|
||||||
|
domainSettingsService: DomainSettingsService;
|
||||||
organizationService: OrganizationService;
|
organizationService: OrganizationService;
|
||||||
providerService: ProviderService;
|
providerService: ProviderService;
|
||||||
twoFactorService: TwoFactorService;
|
twoFactorService: TwoFactorService;
|
||||||
@ -333,7 +338,7 @@ export class Main {
|
|||||||
this.stateProvider,
|
this.stateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.appIdService = new AppIdService(this.storageService);
|
this.appIdService = new AppIdService(this.globalStateProvider);
|
||||||
this.tokenService = new TokenService(this.stateService);
|
this.tokenService = new TokenService(this.stateService);
|
||||||
|
|
||||||
const customUserAgent =
|
const customUserAgent =
|
||||||
@ -358,6 +363,7 @@ export class Main {
|
|||||||
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
this.containerService = new ContainerService(this.cryptoService, this.encryptService);
|
||||||
|
|
||||||
this.settingsService = new SettingsService(this.stateService);
|
this.settingsService = new SettingsService(this.stateService);
|
||||||
|
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
|
||||||
|
|
||||||
this.fileUploadService = new FileUploadService(this.logService);
|
this.fileUploadService = new FileUploadService(this.logService);
|
||||||
|
|
||||||
@ -458,6 +464,7 @@ export class Main {
|
|||||||
this.policyService,
|
this.policyService,
|
||||||
this.deviceTrustCryptoService,
|
this.deviceTrustCryptoService,
|
||||||
this.authRequestService,
|
this.authRequestService,
|
||||||
|
this.globalStateProvider,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.authService = new AuthService(
|
this.authService = new AuthService(
|
||||||
@ -480,7 +487,7 @@ export class Main {
|
|||||||
|
|
||||||
this.cipherService = new CipherService(
|
this.cipherService = new CipherService(
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.settingsService,
|
this.domainSettingsService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
this.searchService,
|
this.searchService,
|
||||||
@ -551,7 +558,7 @@ export class Main {
|
|||||||
|
|
||||||
this.syncService = new SyncService(
|
this.syncService = new SyncService(
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.settingsService,
|
this.domainSettingsService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
@ -647,7 +654,6 @@ export class Main {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.syncService.setLastSync(new Date(0)),
|
this.syncService.setLastSync(new Date(0)),
|
||||||
this.cryptoService.clearKeys(),
|
this.cryptoService.clearKeys(),
|
||||||
this.settingsService.clear(userId),
|
|
||||||
this.cipherService.clear(userId),
|
this.cipherService.clear(userId),
|
||||||
this.folderService.clear(userId),
|
this.folderService.clear(userId),
|
||||||
this.collectionService.clear(userId as UserId),
|
this.collectionService.clear(userId as UserId),
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const nodeExternals = require("webpack-node-externals");
|
const nodeExternals = require("webpack-node-externals");
|
||||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||||
@ -23,7 +22,6 @@ const moduleRules = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
new CleanWebpackPlugin(),
|
|
||||||
new CopyWebpackPlugin({
|
new CopyWebpackPlugin({
|
||||||
patterns: [{ from: "./src/locales", to: "locales" }],
|
patterns: [{ from: "./src/locales", to: "locales" }],
|
||||||
}),
|
}),
|
||||||
@ -71,6 +69,7 @@ const webpackConfig = {
|
|||||||
output: {
|
output: {
|
||||||
filename: "[name].js",
|
filename: "[name].js",
|
||||||
path: path.resolve(__dirname, "build"),
|
path: path.resolve(__dirname, "build"),
|
||||||
|
clean: true,
|
||||||
},
|
},
|
||||||
module: { rules: moduleRules },
|
module: { rules: moduleRules },
|
||||||
plugins: plugins,
|
plugins: plugins,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/desktop",
|
"name": "@bitwarden/desktop",
|
||||||
"description": "A secure and free password manager for all of your devices.",
|
"description": "A secure and free password manager for all of your devices.",
|
||||||
"version": "2024.3.0",
|
"version": "2024.3.1",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"bitwarden",
|
"bitwarden",
|
||||||
"password",
|
"password",
|
||||||
|
@ -577,7 +577,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
await this.eventUploadService.uploadEvents(userBeingLoggedOut);
|
||||||
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
|
||||||
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
await this.cryptoService.clearKeys(userBeingLoggedOut);
|
||||||
await this.settingsService.clear(userBeingLoggedOut);
|
|
||||||
await this.cipherService.clear(userBeingLoggedOut);
|
await this.cipherService.clear(userBeingLoggedOut);
|
||||||
await this.folderService.clear(userBeingLoggedOut);
|
await this.folderService.clear(userBeingLoggedOut);
|
||||||
await this.collectionService.clear(userBeingLoggedOut);
|
await this.collectionService.clear(userBeingLoggedOut);
|
||||||
|
4
apps/desktop/src/package-lock.json
generated
4
apps/desktop/src/package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@bitwarden/desktop",
|
"name": "@bitwarden/desktop",
|
||||||
"version": "2024.3.0",
|
"version": "2024.3.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@bitwarden/desktop",
|
"name": "@bitwarden/desktop",
|
||||||
"version": "2024.3.0",
|
"version": "2024.3.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bitwarden/desktop-native": "file:../desktop_native"
|
"@bitwarden/desktop-native": "file:../desktop_native"
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "@bitwarden/desktop",
|
"name": "@bitwarden/desktop",
|
||||||
"productName": "Bitwarden",
|
"productName": "Bitwarden",
|
||||||
"description": "A secure and free password manager for all of your devices.",
|
"description": "A secure and free password manager for all of your devices.",
|
||||||
"version": "2024.3.0",
|
"version": "2024.3.1",
|
||||||
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
"author": "Bitwarden Inc. <hello@bitwarden.com> (https://bitwarden.com)",
|
||||||
"homepage": "https://bitwarden.com",
|
"homepage": "https://bitwarden.com",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { merge } = require("webpack-merge");
|
const { merge } = require("webpack-merge");
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
|
||||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||||
const configurator = require("./config/config");
|
const configurator = require("./config/config");
|
||||||
const { EnvironmentPlugin } = require("webpack");
|
const { EnvironmentPlugin } = require("webpack");
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { merge } = require("webpack-merge");
|
const { merge } = require("webpack-merge");
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
|
||||||
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
|
||||||
const configurator = require("./config/config");
|
const configurator = require("./config/config");
|
||||||
const { EnvironmentPlugin } = require("webpack");
|
const { EnvironmentPlugin } = require("webpack");
|
||||||
|
@ -275,7 +275,6 @@ export class AppComponent implements OnDestroy, OnInit {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.syncService.setLastSync(new Date(0)),
|
this.syncService.setLastSync(new Date(0)),
|
||||||
this.cryptoService.clearKeys(),
|
this.cryptoService.clearKeys(),
|
||||||
this.settingsService.clear(userId),
|
|
||||||
this.cipherService.clear(userId),
|
this.cipherService.clear(userId),
|
||||||
this.folderService.clear(userId),
|
this.folderService.clear(userId),
|
||||||
this.collectionService.clear(userId),
|
this.collectionService.clear(userId),
|
||||||
|
@ -2,7 +2,6 @@ const fs = require("fs");
|
|||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
const { AngularWebpackPlugin } = require("@ngtools/webpack");
|
||||||
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const HtmlWebpackInjector = require("html-webpack-injector");
|
const HtmlWebpackInjector = require("html-webpack-injector");
|
||||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||||
@ -87,7 +86,6 @@ const moduleRules = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const plugins = [
|
const plugins = [
|
||||||
new CleanWebpackPlugin(),
|
|
||||||
new HtmlWebpackPlugin({
|
new HtmlWebpackPlugin({
|
||||||
template: "./src/index.html",
|
template: "./src/index.html",
|
||||||
filename: "index.html",
|
filename: "index.html",
|
||||||
@ -371,6 +369,7 @@ const webpackConfig = {
|
|||||||
output: {
|
output: {
|
||||||
filename: "[name].[contenthash].js",
|
filename: "[name].[contenthash].js",
|
||||||
path: path.resolve(__dirname, "build"),
|
path: path.resolve(__dirname, "build"),
|
||||||
|
clean: true,
|
||||||
},
|
},
|
||||||
module: {
|
module: {
|
||||||
noParse: /\.wasm$/,
|
noParse: /\.wasm$/,
|
||||||
|
@ -99,8 +99,7 @@ export class LoginViaAuthRequestComponent
|
|||||||
}
|
}
|
||||||
|
|
||||||
//gets signalR push notification
|
//gets signalR push notification
|
||||||
this.loginStrategyService
|
this.loginStrategyService.authRequestPushNotification$
|
||||||
.getPushNotificationObs$()
|
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe((id) => {
|
.subscribe((id) => {
|
||||||
// Only fires on approval currently
|
// Only fires on approval currently
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Directive, Inject, OnDestroy, OnInit } from "@angular/core";
|
import { Directive, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
import { ActivatedRoute, NavigationExtras, Router } from "@angular/router";
|
||||||
import * as DuoWebSDK from "duo_web_sdk";
|
import * as DuoWebSDK from "duo_web_sdk";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
@ -10,6 +11,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
|||||||
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
|
||||||
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||||
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason";
|
||||||
@ -92,7 +94,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
if (!this.authing || this.twoFactorService.getProviders() == null) {
|
if (!(await this.authing()) || this.twoFactorService.getProviders() == null) {
|
||||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
this.router.navigate([this.loginRoute]);
|
this.router.navigate([this.loginRoute]);
|
||||||
@ -105,7 +107,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.needsLock) {
|
if (await this.needsLock()) {
|
||||||
this.successRoute = "lock";
|
this.successRoute = "lock";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,7 +428,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.loginStrategyService.email == null) {
|
if ((await this.loginStrategyService.getEmail()) == null) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
this.i18nService.t("errorOccurred"),
|
this.i18nService.t("errorOccurred"),
|
||||||
@ -437,12 +439,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const request = new TwoFactorEmailRequest();
|
const request = new TwoFactorEmailRequest();
|
||||||
request.email = this.loginStrategyService.email;
|
request.email = await this.loginStrategyService.getEmail();
|
||||||
request.masterPasswordHash = this.loginStrategyService.masterPasswordHash;
|
request.masterPasswordHash = await this.loginStrategyService.getMasterPasswordHash();
|
||||||
request.ssoEmail2FaSessionToken = this.loginStrategyService.ssoEmail2FaSessionToken;
|
request.ssoEmail2FaSessionToken =
|
||||||
|
await this.loginStrategyService.getSsoEmail2FaSessionToken();
|
||||||
request.deviceIdentifier = await this.appIdService.getAppId();
|
request.deviceIdentifier = await this.appIdService.getAppId();
|
||||||
request.authRequestAccessCode = this.loginStrategyService.accessCode;
|
request.authRequestAccessCode = await this.loginStrategyService.getAccessCode();
|
||||||
request.authRequestId = this.loginStrategyService.authRequestId;
|
request.authRequestId = await this.loginStrategyService.getAuthRequestId();
|
||||||
this.emailPromise = this.apiService.postTwoFactorEmail(request);
|
this.emailPromise = this.apiService.postTwoFactorEmail(request);
|
||||||
await this.emailPromise;
|
await this.emailPromise;
|
||||||
if (doToast) {
|
if (doToast) {
|
||||||
@ -476,20 +479,13 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get authing(): boolean {
|
private async authing(): Promise<boolean> {
|
||||||
return (
|
return (await firstValueFrom(this.loginStrategyService.currentAuthType$)) !== null;
|
||||||
this.loginStrategyService.authingWithPassword() ||
|
|
||||||
this.loginStrategyService.authingWithSso() ||
|
|
||||||
this.loginStrategyService.authingWithUserApiKey() ||
|
|
||||||
this.loginStrategyService.authingWithPasswordless()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get needsLock(): boolean {
|
private async needsLock(): Promise<boolean> {
|
||||||
return (
|
const authType = await firstValueFrom(this.loginStrategyService.currentAuthType$);
|
||||||
this.loginStrategyService.authingWithSso() ||
|
return authType == AuthenticationType.Sso || authType == AuthenticationType.UserApiKey;
|
||||||
this.loginStrategyService.authingWithUserApiKey()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// implemented in clients
|
// implemented in clients
|
||||||
|
@ -90,6 +90,10 @@ import {
|
|||||||
BadgeSettingsServiceAbstraction,
|
BadgeSettingsServiceAbstraction,
|
||||||
BadgeSettingsService,
|
BadgeSettingsService,
|
||||||
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
} from "@bitwarden/common/autofill/services/badge-settings.service";
|
||||||
|
import {
|
||||||
|
DomainSettingsService,
|
||||||
|
DefaultDomainSettingsService,
|
||||||
|
} from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
|
||||||
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
|
||||||
import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
|
||||||
@ -290,7 +294,7 @@ import { ModalService } from "./modal.service";
|
|||||||
{
|
{
|
||||||
provide: AppIdServiceAbstraction,
|
provide: AppIdServiceAbstraction,
|
||||||
useClass: AppIdService,
|
useClass: AppIdService,
|
||||||
deps: [AbstractStorageService],
|
deps: [GlobalStateProvider],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
provide: AuditServiceAbstraction,
|
provide: AuditServiceAbstraction,
|
||||||
@ -328,6 +332,7 @@ import { ModalService } from "./modal.service";
|
|||||||
PolicyServiceAbstraction,
|
PolicyServiceAbstraction,
|
||||||
DeviceTrustCryptoServiceAbstraction,
|
DeviceTrustCryptoServiceAbstraction,
|
||||||
AuthRequestServiceAbstraction,
|
AuthRequestServiceAbstraction,
|
||||||
|
GlobalStateProvider,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -350,6 +355,7 @@ import { ModalService } from "./modal.service";
|
|||||||
searchService: SearchServiceAbstraction,
|
searchService: SearchServiceAbstraction,
|
||||||
stateService: StateServiceAbstraction,
|
stateService: StateServiceAbstraction,
|
||||||
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
autofillSettingsService: AutofillSettingsServiceAbstraction,
|
||||||
|
domainSettingsService: DomainSettingsService,
|
||||||
encryptService: EncryptService,
|
encryptService: EncryptService,
|
||||||
fileUploadService: CipherFileUploadServiceAbstraction,
|
fileUploadService: CipherFileUploadServiceAbstraction,
|
||||||
configService: ConfigServiceAbstraction,
|
configService: ConfigServiceAbstraction,
|
||||||
@ -357,7 +363,7 @@ import { ModalService } from "./modal.service";
|
|||||||
) =>
|
) =>
|
||||||
new CipherService(
|
new CipherService(
|
||||||
cryptoService,
|
cryptoService,
|
||||||
settingsService,
|
domainSettingsService,
|
||||||
apiService,
|
apiService,
|
||||||
i18nService,
|
i18nService,
|
||||||
searchService,
|
searchService,
|
||||||
@ -739,7 +745,7 @@ import { ModalService } from "./modal.service";
|
|||||||
useClass: PasswordResetEnrollmentServiceImplementation,
|
useClass: PasswordResetEnrollmentServiceImplementation,
|
||||||
deps: [
|
deps: [
|
||||||
OrganizationApiServiceAbstraction,
|
OrganizationApiServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
AccountServiceAbstraction,
|
||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
OrganizationUserService,
|
OrganizationUserService,
|
||||||
I18nServiceAbstraction,
|
I18nServiceAbstraction,
|
||||||
@ -964,6 +970,11 @@ import { ModalService } from "./modal.service";
|
|||||||
useClass: BadgeSettingsService,
|
useClass: BadgeSettingsService,
|
||||||
deps: [StateProvider],
|
deps: [StateProvider],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: DomainSettingsService,
|
||||||
|
useClass: DefaultDomainSettingsService,
|
||||||
|
deps: [StateProvider],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: BiometricStateService,
|
provide: BiometricStateService,
|
||||||
useClass: DefaultBiometricStateService,
|
useClass: DefaultBiometricStateService,
|
||||||
|
@ -13,6 +13,7 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-
|
|||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { EventType } from "@bitwarden/common/enums";
|
import { EventType } from "@bitwarden/common/enums";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
|
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
|
||||||
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
@ -24,7 +25,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
|
|||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
import { CipherType, SecureNoteType, UriMatchType } from "@bitwarden/common/vault/enums";
|
import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
|
||||||
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
|
||||||
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
|
||||||
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
import { CardView } from "@bitwarden/common/vault/models/view/card.view";
|
||||||
@ -164,12 +165,12 @@ export class AddEditComponent implements OnInit, OnDestroy {
|
|||||||
];
|
];
|
||||||
this.uriMatchOptions = [
|
this.uriMatchOptions = [
|
||||||
{ name: i18nService.t("defaultMatchDetection"), value: null },
|
{ name: i18nService.t("defaultMatchDetection"), value: null },
|
||||||
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain },
|
{ name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
|
||||||
{ name: i18nService.t("host"), value: UriMatchType.Host },
|
{ name: i18nService.t("host"), value: UriMatchStrategy.Host },
|
||||||
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith },
|
{ name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
|
||||||
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression },
|
{ name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
|
||||||
{ name: i18nService.t("exact"), value: UriMatchType.Exact },
|
{ name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
|
||||||
{ name: i18nService.t("never"), value: UriMatchType.Never },
|
{ name: i18nService.t("never"), value: UriMatchStrategy.Never },
|
||||||
];
|
];
|
||||||
this.autofillOnPageLoadOptions = [
|
this.autofillOnPageLoadOptions = [
|
||||||
{ name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null },
|
{ name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null },
|
||||||
|
@ -10,7 +10,11 @@ module.exports = {
|
|||||||
displayName: "libs/auth tests",
|
displayName: "libs/auth tests",
|
||||||
preset: "jest-preset-angular",
|
preset: "jest-preset-angular",
|
||||||
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test.setup.ts"],
|
||||||
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, {
|
moduleNameMapper: pathsToModuleNameMapper(
|
||||||
prefix: "<rootDir>/",
|
// lets us use @bitwarden/common/spec in tests
|
||||||
}),
|
{ "@bitwarden/common/spec": ["../common/spec"], ...(compilerOptions?.paths ?? {}) },
|
||||||
|
{
|
||||||
|
prefix: "<rootDir>/",
|
||||||
|
},
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||||
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||||
import { MasterKey } from "@bitwarden/common/types/key";
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
@ -14,12 +16,45 @@ import {
|
|||||||
} from "../models/domain/login-credentials";
|
} from "../models/domain/login-credentials";
|
||||||
|
|
||||||
export abstract class LoginStrategyServiceAbstraction {
|
export abstract class LoginStrategyServiceAbstraction {
|
||||||
masterPasswordHash: string;
|
/**
|
||||||
email: string;
|
* The current strategy being used to authenticate.
|
||||||
accessCode: string;
|
* Emits null if the session has timed out.
|
||||||
authRequestId: string;
|
*/
|
||||||
ssoEmail2FaSessionToken: string;
|
currentAuthType$: Observable<AuthenticationType | null>;
|
||||||
|
/**
|
||||||
|
* Emits when an auth request has been approved.
|
||||||
|
*/
|
||||||
|
authRequestPushNotification$: Observable<string>;
|
||||||
|
/**
|
||||||
|
* If the login strategy uses the email address of the user, this
|
||||||
|
* will return it. Otherwise, it will return null.
|
||||||
|
*/
|
||||||
|
getEmail: () => Promise<string | null>;
|
||||||
|
/**
|
||||||
|
* If the user is logging in with a master password, this will return
|
||||||
|
* the master password hash. Otherwise, it will return null.
|
||||||
|
*/
|
||||||
|
getMasterPasswordHash: () => Promise<string | null>;
|
||||||
|
/**
|
||||||
|
* If the user is logging in with SSO, this will return
|
||||||
|
* the email auth token. Otherwise, it will return null.
|
||||||
|
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
|
||||||
|
*/
|
||||||
|
getSsoEmail2FaSessionToken: () => Promise<string | null>;
|
||||||
|
/**
|
||||||
|
* Returns the access code if the user is logging in with an
|
||||||
|
* Auth Request. Otherwise, it will return null.
|
||||||
|
*/
|
||||||
|
getAccessCode: () => Promise<string | null>;
|
||||||
|
/**
|
||||||
|
* Returns the auth request ID if the user is logging in with an
|
||||||
|
* Auth Request. Otherwise, it will return null.
|
||||||
|
*/
|
||||||
|
getAuthRequestId: () => Promise<string | null>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a token request to the server using the provided credentials.
|
||||||
|
*/
|
||||||
logIn: (
|
logIn: (
|
||||||
credentials:
|
credentials:
|
||||||
| UserApiLoginCredentials
|
| UserApiLoginCredentials
|
||||||
@ -28,15 +63,30 @@ export abstract class LoginStrategyServiceAbstraction {
|
|||||||
| AuthRequestLoginCredentials
|
| AuthRequestLoginCredentials
|
||||||
| WebAuthnLoginCredentials,
|
| WebAuthnLoginCredentials,
|
||||||
) => Promise<AuthResult>;
|
) => Promise<AuthResult>;
|
||||||
|
/**
|
||||||
|
* Sends a token request to the server with the provided two factor token
|
||||||
|
* and captcha response. This uses data stored from {@link LoginStrategyServiceAbstraction.logIn},
|
||||||
|
* so that must be called first.
|
||||||
|
* Returns an error if no session data is found.
|
||||||
|
*/
|
||||||
logInTwoFactor: (
|
logInTwoFactor: (
|
||||||
twoFactor: TokenTwoFactorRequest,
|
twoFactor: TokenTwoFactorRequest,
|
||||||
captchaResponse: string,
|
captchaResponse: string,
|
||||||
) => Promise<AuthResult>;
|
) => Promise<AuthResult>;
|
||||||
|
/**
|
||||||
|
* Creates a master key from the provided master password and email.
|
||||||
|
*/
|
||||||
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
makePreloginKey: (masterPassword: string, email: string) => Promise<MasterKey>;
|
||||||
authingWithUserApiKey: () => boolean;
|
/**
|
||||||
authingWithSso: () => boolean;
|
* Sends a notification to {@link LoginStrategyServiceAbstraction.authRequestPushNotification}
|
||||||
authingWithPassword: () => boolean;
|
*/
|
||||||
authingWithPasswordless: () => boolean;
|
sendAuthRequestPushNotification: (notification: AuthRequestPushNotification) => Promise<void>;
|
||||||
authResponsePushNotification: (notification: AuthRequestPushNotification) => Promise<any>;
|
/**
|
||||||
getPushNotificationObs$: () => Observable<any>;
|
* Sends a response to an auth request.
|
||||||
|
*/
|
||||||
|
passwordlessLogin: (
|
||||||
|
id: string,
|
||||||
|
key: string,
|
||||||
|
requestApproved: boolean,
|
||||||
|
) => Promise<AuthRequestResponse>;
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,15 @@ import { MasterKey, UserKey } from "@bitwarden/common/types/key";
|
|||||||
|
|
||||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
|
||||||
import { AuthRequestLoginStrategy } from "./auth-request-login.strategy";
|
import {
|
||||||
|
AuthRequestLoginStrategy,
|
||||||
|
AuthRequestLoginStrategyData,
|
||||||
|
} from "./auth-request-login.strategy";
|
||||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||||
|
|
||||||
describe("AuthRequestLoginStrategy", () => {
|
describe("AuthRequestLoginStrategy", () => {
|
||||||
|
let cache: AuthRequestLoginStrategyData;
|
||||||
|
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
let tokenService: MockProxy<TokenService>;
|
let tokenService: MockProxy<TokenService>;
|
||||||
@ -65,6 +70,7 @@ describe("AuthRequestLoginStrategy", () => {
|
|||||||
tokenService.decodeToken.mockResolvedValue({});
|
tokenService.decodeToken.mockResolvedValue({});
|
||||||
|
|
||||||
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
authRequestLoginStrategy = new AuthRequestLoginStrategy(
|
||||||
|
cache,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
tokenService,
|
tokenService,
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { Observable, map, BehaviorSubject } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
@ -14,26 +17,33 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
import { AuthRequestLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
import { LoginStrategy } from "./login.strategy";
|
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||||
|
|
||||||
|
export class AuthRequestLoginStrategyData implements LoginStrategyData {
|
||||||
|
tokenRequest: PasswordTokenRequest;
|
||||||
|
captchaBypassToken: string;
|
||||||
|
authRequestCredentials: AuthRequestLoginCredentials;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<AuthRequestLoginStrategyData>): AuthRequestLoginStrategyData {
|
||||||
|
const data = Object.assign(new AuthRequestLoginStrategyData(), obj, {
|
||||||
|
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
|
||||||
|
authRequestCredentials: AuthRequestLoginCredentials.fromJSON(obj.authRequestCredentials),
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class AuthRequestLoginStrategy extends LoginStrategy {
|
export class AuthRequestLoginStrategy extends LoginStrategy {
|
||||||
get email() {
|
email$: Observable<string>;
|
||||||
return this.tokenRequest.email;
|
accessCode$: Observable<string>;
|
||||||
}
|
authRequestId$: Observable<string>;
|
||||||
|
|
||||||
get accessCode() {
|
protected cache: BehaviorSubject<AuthRequestLoginStrategyData>;
|
||||||
return this.authRequestCredentials.accessCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
get authRequestId() {
|
|
||||||
return this.authRequestCredentials.authRequestId;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenRequest: PasswordTokenRequest;
|
|
||||||
private authRequestCredentials: AuthRequestLoginCredentials;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
data: AuthRequestLoginStrategyData,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
tokenService: TokenService,
|
tokenService: TokenService,
|
||||||
@ -56,22 +66,26 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.cache = new BehaviorSubject(data);
|
||||||
|
this.email$ = this.cache.pipe(map((data) => data.tokenRequest.email));
|
||||||
|
this.accessCode$ = this.cache.pipe(map((data) => data.authRequestCredentials.accessCode));
|
||||||
|
this.authRequestId$ = this.cache.pipe(map((data) => data.authRequestCredentials.authRequestId));
|
||||||
}
|
}
|
||||||
|
|
||||||
override async logIn(credentials: AuthRequestLoginCredentials) {
|
override async logIn(credentials: AuthRequestLoginCredentials) {
|
||||||
// NOTE: To avoid DeadObject references on Firefox, do not set the credentials object directly
|
const data = new AuthRequestLoginStrategyData();
|
||||||
// Use deep copy in future if objects are added that were created in popup
|
data.tokenRequest = new PasswordTokenRequest(
|
||||||
this.authRequestCredentials = { ...credentials };
|
|
||||||
|
|
||||||
this.tokenRequest = new PasswordTokenRequest(
|
|
||||||
credentials.email,
|
credentials.email,
|
||||||
credentials.accessCode,
|
credentials.accessCode,
|
||||||
null,
|
null,
|
||||||
await this.buildTwoFactor(credentials.twoFactor),
|
await this.buildTwoFactor(credentials.twoFactor),
|
||||||
await this.buildDeviceRequest(),
|
await this.buildDeviceRequest(),
|
||||||
);
|
);
|
||||||
|
data.tokenRequest.setAuthRequestAccessCode(credentials.authRequestId);
|
||||||
|
data.authRequestCredentials = credentials;
|
||||||
|
this.cache.next(data);
|
||||||
|
|
||||||
this.tokenRequest.setAuthRequestAccessCode(credentials.authRequestId);
|
|
||||||
const [authResult] = await this.startLogIn();
|
const [authResult] = await this.startLogIn();
|
||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
@ -80,27 +94,32 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||||||
twoFactor: TokenTwoFactorRequest,
|
twoFactor: TokenTwoFactorRequest,
|
||||||
captchaResponse: string,
|
captchaResponse: string,
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
const data = this.cache.value;
|
||||||
|
data.tokenRequest.captchaResponse = captchaResponse ?? data.captchaBypassToken;
|
||||||
|
this.cache.next(data);
|
||||||
|
|
||||||
return super.logInTwoFactor(twoFactor);
|
return super.logInTwoFactor(twoFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||||
|
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||||
if (
|
if (
|
||||||
this.authRequestCredentials.decryptedMasterKey &&
|
authRequestCredentials.decryptedMasterKey &&
|
||||||
this.authRequestCredentials.decryptedMasterKeyHash
|
authRequestCredentials.decryptedMasterKeyHash
|
||||||
) {
|
) {
|
||||||
await this.cryptoService.setMasterKey(this.authRequestCredentials.decryptedMasterKey);
|
await this.cryptoService.setMasterKey(authRequestCredentials.decryptedMasterKey);
|
||||||
await this.cryptoService.setMasterKeyHash(this.authRequestCredentials.decryptedMasterKeyHash);
|
await this.cryptoService.setMasterKeyHash(authRequestCredentials.decryptedMasterKeyHash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||||
|
const authRequestCredentials = this.cache.value.authRequestCredentials;
|
||||||
// User now may or may not have a master password
|
// User now may or may not have a master password
|
||||||
// but set the master key encrypted user key if it exists regardless
|
// but set the master key encrypted user key if it exists regardless
|
||||||
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
await this.cryptoService.setMasterKeyEncryptedUserKey(response.key);
|
||||||
|
|
||||||
if (this.authRequestCredentials.decryptedUserKey) {
|
if (authRequestCredentials.decryptedUserKey) {
|
||||||
await this.cryptoService.setUserKey(this.authRequestCredentials.decryptedUserKey);
|
await this.cryptoService.setUserKey(authRequestCredentials.decryptedUserKey);
|
||||||
} else {
|
} else {
|
||||||
await this.trySetUserKeyWithMasterKey();
|
await this.trySetUserKeyWithMasterKey();
|
||||||
// Establish trust if required after setting user key
|
// Establish trust if required after setting user key
|
||||||
@ -121,4 +140,10 @@ export class AuthRequestLoginStrategy extends LoginStrategy {
|
|||||||
response.privateKey ?? (await this.createKeyPairForOldAccount()),
|
response.privateKey ?? (await this.createKeyPairForOldAccount()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportCache(): CacheData {
|
||||||
|
return {
|
||||||
|
authRequest: this.cache.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key";
|
|||||||
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service";
|
import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service";
|
||||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
|
||||||
import { PasswordLoginStrategy } from "./password-login.strategy";
|
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
|
||||||
|
|
||||||
const email = "hello@world.com";
|
const email = "hello@world.com";
|
||||||
const masterPassword = "password";
|
const masterPassword = "password";
|
||||||
@ -94,6 +94,8 @@ export function identityTokenResponseFactory(
|
|||||||
|
|
||||||
// TODO: add tests for latest changes to base class for TDE
|
// TODO: add tests for latest changes to base class for TDE
|
||||||
describe("LoginStrategy", () => {
|
describe("LoginStrategy", () => {
|
||||||
|
let cache: PasswordLoginStrategyData;
|
||||||
|
|
||||||
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
@ -129,6 +131,7 @@ describe("LoginStrategy", () => {
|
|||||||
|
|
||||||
// The base class is abstract so we test it via PasswordLoginStrategy
|
// The base class is abstract so we test it via PasswordLoginStrategy
|
||||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||||
|
cache,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
tokenService,
|
tokenService,
|
||||||
@ -377,11 +380,23 @@ describe("LoginStrategy", () => {
|
|||||||
|
|
||||||
it("sends 2FA token provided by user to server (two-step)", async () => {
|
it("sends 2FA token provided by user to server (two-step)", async () => {
|
||||||
// Simulate a partially completed login
|
// Simulate a partially completed login
|
||||||
passwordLoginStrategy.tokenRequest = new PasswordTokenRequest(
|
cache = new PasswordLoginStrategyData();
|
||||||
email,
|
cache.tokenRequest = new PasswordTokenRequest(email, masterPasswordHash, null, null);
|
||||||
masterPasswordHash,
|
|
||||||
null,
|
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||||
null,
|
cache,
|
||||||
|
cryptoService,
|
||||||
|
apiService,
|
||||||
|
tokenService,
|
||||||
|
appIdService,
|
||||||
|
platformUtilsService,
|
||||||
|
messagingService,
|
||||||
|
logService,
|
||||||
|
stateService,
|
||||||
|
twoFactorService,
|
||||||
|
passwordStrengthService,
|
||||||
|
policyService,
|
||||||
|
loginStrategyService,
|
||||||
);
|
);
|
||||||
|
|
||||||
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
apiService.postIdentityToken.mockResolvedValue(identityTokenResponseFactory());
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
@ -36,16 +38,21 @@ import {
|
|||||||
AuthRequestLoginCredentials,
|
AuthRequestLoginCredentials,
|
||||||
WebAuthnLoginCredentials,
|
WebAuthnLoginCredentials,
|
||||||
} from "../models/domain/login-credentials";
|
} from "../models/domain/login-credentials";
|
||||||
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
|
type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse;
|
||||||
|
|
||||||
export abstract class LoginStrategy {
|
export abstract class LoginStrategyData {
|
||||||
protected abstract tokenRequest:
|
tokenRequest:
|
||||||
| UserApiTokenRequest
|
| UserApiTokenRequest
|
||||||
| PasswordTokenRequest
|
| PasswordTokenRequest
|
||||||
| SsoTokenRequest
|
| SsoTokenRequest
|
||||||
| WebAuthnLoginTokenRequest;
|
| WebAuthnLoginTokenRequest;
|
||||||
protected captchaBypassToken: string = null;
|
captchaBypassToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class LoginStrategy {
|
||||||
|
protected abstract cache: BehaviorSubject<LoginStrategyData>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
@ -59,6 +66,8 @@ export abstract class LoginStrategy {
|
|||||||
protected twoFactorService: TwoFactorService,
|
protected twoFactorService: TwoFactorService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
abstract exportCache(): CacheData;
|
||||||
|
|
||||||
abstract logIn(
|
abstract logIn(
|
||||||
credentials:
|
credentials:
|
||||||
| UserApiLoginCredentials
|
| UserApiLoginCredentials
|
||||||
@ -72,7 +81,9 @@ export abstract class LoginStrategy {
|
|||||||
twoFactor: TokenTwoFactorRequest,
|
twoFactor: TokenTwoFactorRequest,
|
||||||
captchaResponse: string = null,
|
captchaResponse: string = null,
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
this.tokenRequest.setTwoFactor(twoFactor);
|
const data = this.cache.value;
|
||||||
|
data.tokenRequest.setTwoFactor(twoFactor);
|
||||||
|
this.cache.next(data);
|
||||||
const [authResult] = await this.startLogIn();
|
const [authResult] = await this.startLogIn();
|
||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
@ -80,7 +91,8 @@ export abstract class LoginStrategy {
|
|||||||
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
|
protected async startLogIn(): Promise<[AuthResult, IdentityResponse]> {
|
||||||
this.twoFactorService.clearSelectedProvider();
|
this.twoFactorService.clearSelectedProvider();
|
||||||
|
|
||||||
const response = await this.apiService.postIdentityToken(this.tokenRequest);
|
const tokenRequest = this.cache.value.tokenRequest;
|
||||||
|
const response = await this.apiService.postIdentityToken(tokenRequest);
|
||||||
|
|
||||||
if (response instanceof IdentityTwoFactorResponse) {
|
if (response instanceof IdentityTwoFactorResponse) {
|
||||||
return [await this.processTwoFactorResponse(response), response];
|
return [await this.processTwoFactorResponse(response), response];
|
||||||
@ -195,9 +207,7 @@ export abstract class LoginStrategy {
|
|||||||
|
|
||||||
// The keys comes from different sources depending on the login strategy
|
// The keys comes from different sources depending on the login strategy
|
||||||
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setMasterKey(response: IdentityTokenResponse): Promise<void>;
|
||||||
|
|
||||||
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setUserKey(response: IdentityTokenResponse): Promise<void>;
|
||||||
|
|
||||||
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
protected abstract setPrivateKey(response: IdentityTokenResponse): Promise<void>;
|
||||||
|
|
||||||
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
// Old accounts used master key for encryption. We are forcing migrations but only need to
|
||||||
@ -221,7 +231,7 @@ export abstract class LoginStrategy {
|
|||||||
result.twoFactorProviders = response.twoFactorProviders2;
|
result.twoFactorProviders = response.twoFactorProviders2;
|
||||||
|
|
||||||
this.twoFactorService.setProviders(response);
|
this.twoFactorService.setProviders(response);
|
||||||
this.captchaBypassToken = response.captchaToken ?? null;
|
this.cache.next({ ...this.cache.value, captchaBypassToken: response.captchaToken ?? null });
|
||||||
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
|
result.ssoEmail2FaSessionToken = response.ssoEmail2faSessionToken;
|
||||||
result.email = response.email;
|
result.email = response.email;
|
||||||
return result;
|
return result;
|
||||||
|
@ -29,7 +29,7 @@ import { LoginStrategyServiceAbstraction } from "../abstractions";
|
|||||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
|
||||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||||
import { PasswordLoginStrategy } from "./password-login.strategy";
|
import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy";
|
||||||
|
|
||||||
const email = "hello@world.com";
|
const email = "hello@world.com";
|
||||||
const masterPassword = "password";
|
const masterPassword = "password";
|
||||||
@ -47,6 +47,8 @@ const masterPasswordPolicy = new MasterPasswordPolicyResponse({
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("PasswordLoginStrategy", () => {
|
describe("PasswordLoginStrategy", () => {
|
||||||
|
let cache: PasswordLoginStrategyData;
|
||||||
|
|
||||||
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
let loginStrategyService: MockProxy<LoginStrategyServiceAbstraction>;
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
@ -93,6 +95,7 @@ describe("PasswordLoginStrategy", () => {
|
|||||||
policyService.evaluateMasterPassword.mockReturnValue(true);
|
policyService.evaluateMasterPassword.mockReturnValue(true);
|
||||||
|
|
||||||
passwordLoginStrategy = new PasswordLoginStrategy(
|
passwordLoginStrategy = new PasswordLoginStrategy(
|
||||||
|
cache,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
tokenService,
|
tokenService,
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { BehaviorSubject, map, Observable } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options";
|
||||||
@ -17,35 +20,56 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
import { HashPurpose } from "@bitwarden/common/platform/enums";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { MasterKey } from "@bitwarden/common/types/key";
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
import { LoginStrategyServiceAbstraction } from "../abstractions";
|
||||||
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
import { PasswordLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
import { LoginStrategy } from "./login.strategy";
|
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||||
|
|
||||||
|
export class PasswordLoginStrategyData implements LoginStrategyData {
|
||||||
|
tokenRequest: PasswordTokenRequest;
|
||||||
|
captchaBypassToken?: string;
|
||||||
|
/**
|
||||||
|
* The local version of the user's master key hash
|
||||||
|
*/
|
||||||
|
localMasterKeyHash: string;
|
||||||
|
/**
|
||||||
|
* The user's master key
|
||||||
|
*/
|
||||||
|
masterKey: MasterKey;
|
||||||
|
/**
|
||||||
|
* Tracks if the user needs to update their password due to
|
||||||
|
* a password that does not meet an organization's master password policy.
|
||||||
|
*/
|
||||||
|
forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<PasswordLoginStrategyData>): PasswordLoginStrategyData {
|
||||||
|
const data = Object.assign(new PasswordLoginStrategyData(), obj, {
|
||||||
|
tokenRequest: PasswordTokenRequest.fromJSON(obj.tokenRequest),
|
||||||
|
masterKey: SymmetricCryptoKey.fromJSON(obj.masterKey),
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class PasswordLoginStrategy extends LoginStrategy {
|
export class PasswordLoginStrategy extends LoginStrategy {
|
||||||
get email() {
|
|
||||||
return this.tokenRequest.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
get masterPasswordHash() {
|
|
||||||
return this.tokenRequest.masterPasswordHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenRequest: PasswordTokenRequest;
|
|
||||||
|
|
||||||
private localMasterKeyHash: string;
|
|
||||||
private masterKey: MasterKey;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Options to track if the user needs to update their password due to a password that does not meet an organization's
|
* The email address of the user attempting to log in.
|
||||||
* master password policy.
|
|
||||||
*/
|
*/
|
||||||
private forcePasswordResetReason: ForceSetPasswordReason = ForceSetPasswordReason.None;
|
email$: Observable<string>;
|
||||||
|
/**
|
||||||
|
* The master key hash of the user attempting to log in.
|
||||||
|
*/
|
||||||
|
masterKeyHash$: Observable<string | null>;
|
||||||
|
|
||||||
|
protected cache: BehaviorSubject<PasswordLoginStrategyData>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
data: PasswordLoginStrategyData,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
tokenService: TokenService,
|
tokenService: TokenService,
|
||||||
@ -70,42 +94,27 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
override async logInTwoFactor(
|
this.cache = new BehaviorSubject(data);
|
||||||
twoFactor: TokenTwoFactorRequest,
|
this.email$ = this.cache.pipe(map((state) => state.tokenRequest.email));
|
||||||
captchaResponse: string,
|
this.masterKeyHash$ = this.cache.pipe(map((state) => state.localMasterKeyHash));
|
||||||
): Promise<AuthResult> {
|
|
||||||
this.tokenRequest.captchaResponse = captchaResponse ?? this.captchaBypassToken;
|
|
||||||
const result = await super.logInTwoFactor(twoFactor);
|
|
||||||
|
|
||||||
// 2FA was successful, save the force update password options with the state service if defined
|
|
||||||
if (
|
|
||||||
!result.requiresTwoFactor &&
|
|
||||||
!result.requiresCaptcha &&
|
|
||||||
this.forcePasswordResetReason != ForceSetPasswordReason.None
|
|
||||||
) {
|
|
||||||
await this.stateService.setForceSetPasswordReason(this.forcePasswordResetReason);
|
|
||||||
result.forcePasswordReset = this.forcePasswordResetReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override async logIn(credentials: PasswordLoginCredentials) {
|
override async logIn(credentials: PasswordLoginCredentials) {
|
||||||
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
const { email, masterPassword, captchaToken, twoFactor } = credentials;
|
||||||
|
|
||||||
this.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
|
const data = new PasswordLoginStrategyData();
|
||||||
|
data.masterKey = await this.loginStrategyService.makePreloginKey(masterPassword, email);
|
||||||
|
|
||||||
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
|
// Hash the password early (before authentication) so we don't persist it in memory in plaintext
|
||||||
this.localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
data.localMasterKeyHash = await this.cryptoService.hashMasterKey(
|
||||||
masterPassword,
|
masterPassword,
|
||||||
this.masterKey,
|
data.masterKey,
|
||||||
HashPurpose.LocalAuthorization,
|
HashPurpose.LocalAuthorization,
|
||||||
);
|
);
|
||||||
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, this.masterKey);
|
const masterKeyHash = await this.cryptoService.hashMasterKey(masterPassword, data.masterKey);
|
||||||
|
|
||||||
this.tokenRequest = new PasswordTokenRequest(
|
data.tokenRequest = new PasswordTokenRequest(
|
||||||
email,
|
email,
|
||||||
masterKeyHash,
|
masterKeyHash,
|
||||||
captchaToken,
|
captchaToken,
|
||||||
@ -113,6 +122,8 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
await this.buildDeviceRequest(),
|
await this.buildDeviceRequest(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.cache.next(data);
|
||||||
|
|
||||||
const [authResult, identityResponse] = await this.startLogIn();
|
const [authResult, identityResponse] = await this.startLogIn();
|
||||||
|
|
||||||
const masterPasswordPolicyOptions =
|
const masterPasswordPolicyOptions =
|
||||||
@ -129,7 +140,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
if (!meetsRequirements) {
|
if (!meetsRequirements) {
|
||||||
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
|
if (authResult.requiresCaptcha || authResult.requiresTwoFactor) {
|
||||||
// Save the flag to this strategy for later use as the master password is about to pass out of scope
|
// Save the flag to this strategy for later use as the master password is about to pass out of scope
|
||||||
this.forcePasswordResetReason = ForceSetPasswordReason.WeakMasterPassword;
|
this.cache.next({
|
||||||
|
...this.cache.value,
|
||||||
|
forcePasswordResetReason: ForceSetPasswordReason.WeakMasterPassword,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Authentication was successful, save the force update password options with the state service
|
// Authentication was successful, save the force update password options with the state service
|
||||||
await this.stateService.setForceSetPasswordReason(
|
await this.stateService.setForceSetPasswordReason(
|
||||||
@ -142,9 +156,34 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
return authResult;
|
return authResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override async logInTwoFactor(
|
||||||
|
twoFactor: TokenTwoFactorRequest,
|
||||||
|
captchaResponse: string,
|
||||||
|
): Promise<AuthResult> {
|
||||||
|
this.cache.next({
|
||||||
|
...this.cache.value,
|
||||||
|
captchaBypassToken: captchaResponse ?? this.cache.value.captchaBypassToken,
|
||||||
|
});
|
||||||
|
const result = await super.logInTwoFactor(twoFactor);
|
||||||
|
|
||||||
|
// 2FA was successful, save the force update password options with the state service if defined
|
||||||
|
const forcePasswordResetReason = this.cache.value.forcePasswordResetReason;
|
||||||
|
if (
|
||||||
|
!result.requiresTwoFactor &&
|
||||||
|
!result.requiresCaptcha &&
|
||||||
|
forcePasswordResetReason != ForceSetPasswordReason.None
|
||||||
|
) {
|
||||||
|
await this.stateService.setForceSetPasswordReason(forcePasswordResetReason);
|
||||||
|
result.forcePasswordReset = forcePasswordResetReason;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
protected override async setMasterKey(response: IdentityTokenResponse) {
|
protected override async setMasterKey(response: IdentityTokenResponse) {
|
||||||
await this.cryptoService.setMasterKey(this.masterKey);
|
const { masterKey, localMasterKeyHash } = this.cache.value;
|
||||||
await this.cryptoService.setMasterKeyHash(this.localMasterKeyHash);
|
await this.cryptoService.setMasterKey(masterKey);
|
||||||
|
await this.cryptoService.setMasterKeyHash(localMasterKeyHash);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
protected override async setUserKey(response: IdentityTokenResponse): Promise<void> {
|
||||||
@ -191,4 +230,10 @@ export class PasswordLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
|
return this.policyService.evaluateMasterPassword(passwordStrength, masterPassword, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportCache(): CacheData {
|
||||||
|
return {
|
||||||
|
password: this.cache.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,11 @@ import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abst
|
|||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { AdminAuthRequestStorable } from "@bitwarden/common/auth/models/domain/admin-auth-req-storable";
|
||||||
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||||
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
import { IUserDecryptionOptionsServerResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/user-decryption-options.response";
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
@ -25,9 +28,6 @@ import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
|||||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||||
import { SsoLoginStrategy } from "./sso-login.strategy";
|
import { SsoLoginStrategy } from "./sso-login.strategy";
|
||||||
|
|
||||||
// TODO: Add tests for new trySetUserKeyWithApprovedAdminRequestIfExists logic
|
|
||||||
// https://bitwarden.atlassian.net/browse/PM-3339
|
|
||||||
|
|
||||||
describe("SsoLoginStrategy", () => {
|
describe("SsoLoginStrategy", () => {
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
@ -74,6 +74,7 @@ describe("SsoLoginStrategy", () => {
|
|||||||
tokenService.decodeToken.mockResolvedValue({});
|
tokenService.decodeToken.mockResolvedValue({});
|
||||||
|
|
||||||
ssoLoginStrategy = new SsoLoginStrategy(
|
ssoLoginStrategy = new SsoLoginStrategy(
|
||||||
|
null,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
tokenService,
|
tokenService,
|
||||||
@ -258,6 +259,114 @@ describe("SsoLoginStrategy", () => {
|
|||||||
// Assert
|
// Assert
|
||||||
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
expect(cryptoService.setUserKey).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("AdminAuthRequest", () => {
|
||||||
|
let tokenResponse: IdentityTokenResponse;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
tokenResponse = identityTokenResponseFactory(null, {
|
||||||
|
HasMasterPassword: true,
|
||||||
|
TrustedDeviceOption: {
|
||||||
|
HasAdminApproval: true,
|
||||||
|
HasLoginApprovingDevice: false,
|
||||||
|
HasManageResetPasswordPermission: false,
|
||||||
|
EncryptedPrivateKey: mockEncDevicePrivateKey,
|
||||||
|
EncryptedUserKey: mockEncUserKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminAuthRequest = {
|
||||||
|
id: "1",
|
||||||
|
privateKey: "PRIVATE" as any,
|
||||||
|
} as AdminAuthRequestStorable;
|
||||||
|
stateService.getAdminAuthRequest.mockResolvedValue(
|
||||||
|
new AdminAuthRequestStorable(adminAuthRequest),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the user key using master key and hash from approved admin request if exists", async () => {
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
cryptoService.hasUserKey.mockResolvedValue(true);
|
||||||
|
const adminAuthResponse = {
|
||||||
|
id: "1",
|
||||||
|
publicKey: "PRIVATE" as any,
|
||||||
|
key: "KEY" as any,
|
||||||
|
masterPasswordHash: "HASH" as any,
|
||||||
|
requestApproved: true,
|
||||||
|
};
|
||||||
|
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||||
|
|
||||||
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash).toHaveBeenCalled();
|
||||||
|
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the user key from approved admin request if exists", async () => {
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
cryptoService.hasUserKey.mockResolvedValue(true);
|
||||||
|
const adminAuthResponse = {
|
||||||
|
id: "1",
|
||||||
|
publicKey: "PRIVATE" as any,
|
||||||
|
key: "KEY" as any,
|
||||||
|
requestApproved: true,
|
||||||
|
};
|
||||||
|
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||||
|
|
||||||
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled();
|
||||||
|
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attempts to establish a trusted device if successful", async () => {
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
cryptoService.hasUserKey.mockResolvedValue(true);
|
||||||
|
const adminAuthResponse = {
|
||||||
|
id: "1",
|
||||||
|
publicKey: "PRIVATE" as any,
|
||||||
|
key: "KEY" as any,
|
||||||
|
requestApproved: true,
|
||||||
|
};
|
||||||
|
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||||
|
|
||||||
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).toHaveBeenCalled();
|
||||||
|
expect(deviceTrustCryptoService.trustDeviceIfRequired).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the admin auth request if server returns a 404, meaning it was deleted", async () => {
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
apiService.getAuthRequest.mockRejectedValue(new ErrorResponse(null, 404));
|
||||||
|
|
||||||
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(stateService.setAdminAuthRequest).toHaveBeenCalledWith(null);
|
||||||
|
expect(
|
||||||
|
authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash,
|
||||||
|
).not.toHaveBeenCalled();
|
||||||
|
expect(authRequestService.setUserKeyAfterDecryptingSharedUserKey).not.toHaveBeenCalled();
|
||||||
|
expect(deviceTrustCryptoService.trustDeviceIfRequired).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("attempts to login with a trusted device if admin auth request isn't successful", async () => {
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(tokenResponse);
|
||||||
|
const adminAuthResponse = {
|
||||||
|
id: "1",
|
||||||
|
publicKey: "PRIVATE" as any,
|
||||||
|
key: "KEY" as any,
|
||||||
|
requestApproved: true,
|
||||||
|
};
|
||||||
|
apiService.getAuthRequest.mockResolvedValue(adminAuthResponse as AuthRequestResponse);
|
||||||
|
cryptoService.hasUserKey.mockResolvedValue(false);
|
||||||
|
deviceTrustCryptoService.getDeviceKey.mockResolvedValue("DEVICE_KEY" as any);
|
||||||
|
|
||||||
|
await ssoLoginStrategy.logIn(credentials);
|
||||||
|
|
||||||
|
expect(deviceTrustCryptoService.decryptUserKeyWithDeviceKey).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Key Connector", () => {
|
describe("Key Connector", () => {
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { Observable, map, BehaviorSubject } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
@ -19,20 +22,54 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv
|
|||||||
|
|
||||||
import { AuthRequestServiceAbstraction } from "../abstractions";
|
import { AuthRequestServiceAbstraction } from "../abstractions";
|
||||||
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
import { SsoLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
import { LoginStrategy } from "./login.strategy";
|
import { LoginStrategyData, LoginStrategy } from "./login.strategy";
|
||||||
|
|
||||||
|
export class SsoLoginStrategyData implements LoginStrategyData {
|
||||||
|
captchaBypassToken: string;
|
||||||
|
tokenRequest: SsoTokenRequest;
|
||||||
|
/**
|
||||||
|
* User email address. Only available after authentication.
|
||||||
|
*/
|
||||||
|
email?: string;
|
||||||
|
/**
|
||||||
|
* The organization ID that the user is logging into. Used for Key Connector
|
||||||
|
* purposes after authentication.
|
||||||
|
*/
|
||||||
|
orgId: string;
|
||||||
|
/**
|
||||||
|
* A token provided by the server as an authentication factor for sending
|
||||||
|
* email OTPs to the user's configured 2FA email address. This is required
|
||||||
|
* as we don't have a master password hash or other verifiable secret when using SSO.
|
||||||
|
*/
|
||||||
|
ssoEmail2FaSessionToken?: string;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<SsoLoginStrategyData>): SsoLoginStrategyData {
|
||||||
|
return Object.assign(new SsoLoginStrategyData(), obj, {
|
||||||
|
tokenRequest: SsoTokenRequest.fromJSON(obj.tokenRequest),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class SsoLoginStrategy extends LoginStrategy {
|
export class SsoLoginStrategy extends LoginStrategy {
|
||||||
tokenRequest: SsoTokenRequest;
|
/**
|
||||||
orgId: string;
|
* @see {@link SsoLoginStrategyData.email}
|
||||||
|
*/
|
||||||
|
email$: Observable<string | null>;
|
||||||
|
/**
|
||||||
|
* @see {@link SsoLoginStrategyData.orgId}
|
||||||
|
*/
|
||||||
|
orgId$: Observable<string>;
|
||||||
|
/**
|
||||||
|
* @see {@link SsoLoginStrategyData.ssoEmail2FaSessionToken}
|
||||||
|
*/
|
||||||
|
ssoEmail2FaSessionToken$: Observable<string | null>;
|
||||||
|
|
||||||
// A session token server side to serve as an authentication factor for the user
|
protected cache: BehaviorSubject<SsoLoginStrategyData>;
|
||||||
// in order to send email OTPs to the user's configured 2FA email address
|
|
||||||
// as we don't have a master password hash or other verifiable secret when using SSO.
|
|
||||||
ssoEmail2FaSessionToken?: string;
|
|
||||||
email?: string; // email not preserved through SSO process so get from server
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
data: SsoLoginStrategyData,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
tokenService: TokenService,
|
tokenService: TokenService,
|
||||||
@ -58,11 +95,17 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.cache = new BehaviorSubject(data);
|
||||||
|
this.email$ = this.cache.pipe(map((state) => state.email));
|
||||||
|
this.orgId$ = this.cache.pipe(map((state) => state.orgId));
|
||||||
|
this.ssoEmail2FaSessionToken$ = this.cache.pipe(map((state) => state.ssoEmail2FaSessionToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
async logIn(credentials: SsoLoginCredentials) {
|
async logIn(credentials: SsoLoginCredentials) {
|
||||||
this.orgId = credentials.orgId;
|
const data = new SsoLoginStrategyData();
|
||||||
this.tokenRequest = new SsoTokenRequest(
|
data.orgId = credentials.orgId;
|
||||||
|
data.tokenRequest = new SsoTokenRequest(
|
||||||
credentials.code,
|
credentials.code,
|
||||||
credentials.codeVerifier,
|
credentials.codeVerifier,
|
||||||
credentials.redirectUrl,
|
credentials.redirectUrl,
|
||||||
@ -70,16 +113,24 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
await this.buildDeviceRequest(),
|
await this.buildDeviceRequest(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.cache.next(data);
|
||||||
|
|
||||||
const [ssoAuthResult] = await this.startLogIn();
|
const [ssoAuthResult] = await this.startLogIn();
|
||||||
|
|
||||||
this.email = ssoAuthResult.email;
|
const email = ssoAuthResult.email;
|
||||||
this.ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
|
const ssoEmail2FaSessionToken = ssoAuthResult.ssoEmail2FaSessionToken;
|
||||||
|
|
||||||
// Auth guard currently handles redirects for this.
|
// Auth guard currently handles redirects for this.
|
||||||
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
if (ssoAuthResult.forcePasswordReset == ForceSetPasswordReason.AdminForcePasswordReset) {
|
||||||
await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset);
|
await this.stateService.setForceSetPasswordReason(ssoAuthResult.forcePasswordReset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.cache.next({
|
||||||
|
...this.cache.value,
|
||||||
|
email,
|
||||||
|
ssoEmail2FaSessionToken,
|
||||||
|
});
|
||||||
|
|
||||||
return ssoAuthResult;
|
return ssoAuthResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +143,10 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
// The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector.
|
// The presence of a masterKeyEncryptedUserKey indicates that the user has already been provisioned in Key Connector.
|
||||||
const newSsoUser = tokenResponse.key == null;
|
const newSsoUser = tokenResponse.key == null;
|
||||||
if (newSsoUser) {
|
if (newSsoUser) {
|
||||||
await this.keyConnectorService.convertNewSsoUserToKeyConnector(tokenResponse, this.orgId);
|
await this.keyConnectorService.convertNewSsoUserToKeyConnector(
|
||||||
|
tokenResponse,
|
||||||
|
this.cache.value.orgId,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
const keyConnectorUrl = this.getKeyConnectorUrl(tokenResponse);
|
||||||
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
await this.keyConnectorService.setMasterKeyFromUrl(keyConnectorUrl);
|
||||||
@ -272,4 +326,10 @@ export class SsoLoginStrategy extends LoginStrategy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exportCache(): CacheData {
|
||||||
|
return {
|
||||||
|
sso: this.cache.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,11 @@ import { UserKey, MasterKey } from "@bitwarden/common/types/key";
|
|||||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
|
||||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||||
import { UserApiLoginStrategy } from "./user-api-login.strategy";
|
import { UserApiLoginStrategy, UserApiLoginStrategyData } from "./user-api-login.strategy";
|
||||||
|
|
||||||
describe("UserApiLoginStrategy", () => {
|
describe("UserApiLoginStrategy", () => {
|
||||||
|
let cache: UserApiLoginStrategyData;
|
||||||
|
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
let tokenService: MockProxy<TokenService>;
|
let tokenService: MockProxy<TokenService>;
|
||||||
@ -60,6 +62,7 @@ describe("UserApiLoginStrategy", () => {
|
|||||||
tokenService.decodeToken.mockResolvedValue({});
|
tokenService.decodeToken.mockResolvedValue({});
|
||||||
|
|
||||||
apiLogInStrategy = new UserApiLoginStrategy(
|
apiLogInStrategy = new UserApiLoginStrategy(
|
||||||
|
cache,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
tokenService,
|
tokenService,
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
@ -13,13 +16,26 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl
|
|||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
|
||||||
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
import { UserApiLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
import { LoginStrategy } from "./login.strategy";
|
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||||
|
|
||||||
|
export class UserApiLoginStrategyData implements LoginStrategyData {
|
||||||
|
tokenRequest: UserApiTokenRequest;
|
||||||
|
captchaBypassToken: string;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<UserApiLoginStrategyData>): UserApiLoginStrategyData {
|
||||||
|
return Object.assign(new UserApiLoginStrategyData(), obj, {
|
||||||
|
tokenRequest: UserApiTokenRequest.fromJSON(obj.tokenRequest),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class UserApiLoginStrategy extends LoginStrategy {
|
export class UserApiLoginStrategy extends LoginStrategy {
|
||||||
tokenRequest: UserApiTokenRequest;
|
protected cache: BehaviorSubject<UserApiLoginStrategyData>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
data: UserApiLoginStrategyData,
|
||||||
cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
tokenService: TokenService,
|
tokenService: TokenService,
|
||||||
@ -43,15 +59,18 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||||||
stateService,
|
stateService,
|
||||||
twoFactorService,
|
twoFactorService,
|
||||||
);
|
);
|
||||||
|
this.cache = new BehaviorSubject(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
override async logIn(credentials: UserApiLoginCredentials) {
|
override async logIn(credentials: UserApiLoginCredentials) {
|
||||||
this.tokenRequest = new UserApiTokenRequest(
|
const data = new UserApiLoginStrategyData();
|
||||||
|
data.tokenRequest = new UserApiTokenRequest(
|
||||||
credentials.clientId,
|
credentials.clientId,
|
||||||
credentials.clientSecret,
|
credentials.clientSecret,
|
||||||
await this.buildTwoFactor(),
|
await this.buildTwoFactor(),
|
||||||
await this.buildDeviceRequest(),
|
await this.buildDeviceRequest(),
|
||||||
);
|
);
|
||||||
|
this.cache.next(data);
|
||||||
|
|
||||||
const [authResult] = await this.startLogIn();
|
const [authResult] = await this.startLogIn();
|
||||||
return authResult;
|
return authResult;
|
||||||
@ -84,7 +103,15 @@ export class UserApiLoginStrategy extends LoginStrategy {
|
|||||||
|
|
||||||
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
protected async saveAccountInformation(tokenResponse: IdentityTokenResponse) {
|
||||||
await super.saveAccountInformation(tokenResponse);
|
await super.saveAccountInformation(tokenResponse);
|
||||||
await this.stateService.setApiKeyClientId(this.tokenRequest.clientId);
|
|
||||||
await this.stateService.setApiKeyClientSecret(this.tokenRequest.clientSecret);
|
const tokenRequest = this.cache.value.tokenRequest;
|
||||||
|
await this.stateService.setApiKeyClientId(tokenRequest.clientId);
|
||||||
|
await this.stateService.setApiKeyClientSecret(tokenRequest.clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportCache(): CacheData {
|
||||||
|
return {
|
||||||
|
userApiKey: this.cache.value,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,11 @@ import { PrfKey, UserKey } from "@bitwarden/common/types/key";
|
|||||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
|
||||||
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
import { identityTokenResponseFactory } from "./login.strategy.spec";
|
||||||
import { WebAuthnLoginStrategy } from "./webauthn-login.strategy";
|
import { WebAuthnLoginStrategy, WebAuthnLoginStrategyData } from "./webauthn-login.strategy";
|
||||||
|
|
||||||
describe("WebAuthnLoginStrategy", () => {
|
describe("WebAuthnLoginStrategy", () => {
|
||||||
|
let cache: WebAuthnLoginStrategyData;
|
||||||
|
|
||||||
let cryptoService!: MockProxy<CryptoService>;
|
let cryptoService!: MockProxy<CryptoService>;
|
||||||
let apiService!: MockProxy<ApiService>;
|
let apiService!: MockProxy<ApiService>;
|
||||||
let tokenService!: MockProxy<TokenService>;
|
let tokenService!: MockProxy<TokenService>;
|
||||||
@ -72,6 +74,7 @@ describe("WebAuthnLoginStrategy", () => {
|
|||||||
tokenService.decodeToken.mockResolvedValue({});
|
tokenService.decodeToken.mockResolvedValue({});
|
||||||
|
|
||||||
webAuthnLoginStrategy = new WebAuthnLoginStrategy(
|
webAuthnLoginStrategy = new WebAuthnLoginStrategy(
|
||||||
|
cache,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
tokenService,
|
tokenService,
|
||||||
@ -286,7 +289,7 @@ function randomBytes(length: number): Uint8Array {
|
|||||||
// AuthenticatorAssertionResponse && PublicKeyCredential are only available in secure contexts
|
// AuthenticatorAssertionResponse && PublicKeyCredential are only available in secure contexts
|
||||||
// so we need to mock them and assign them to the global object to make them available
|
// so we need to mock them and assign them to the global object to make them available
|
||||||
// for the tests
|
// for the tests
|
||||||
class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionResponse {
|
export class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionResponse {
|
||||||
clientDataJSON: ArrayBuffer = randomBytes(32).buffer;
|
clientDataJSON: ArrayBuffer = randomBytes(32).buffer;
|
||||||
authenticatorData: ArrayBuffer = randomBytes(196).buffer;
|
authenticatorData: ArrayBuffer = randomBytes(196).buffer;
|
||||||
signature: ArrayBuffer = randomBytes(72).buffer;
|
signature: ArrayBuffer = randomBytes(72).buffer;
|
||||||
@ -298,7 +301,7 @@ class MockAuthenticatorAssertionResponse implements AuthenticatorAssertionRespon
|
|||||||
userHandleB64Str = Utils.fromBufferToUrlB64(this.userHandle);
|
userHandleB64Str = Utils.fromBufferToUrlB64(this.userHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
class MockPublicKeyCredential implements PublicKeyCredential {
|
export class MockPublicKeyCredential implements PublicKeyCredential {
|
||||||
authenticatorAttachment = "cross-platform";
|
authenticatorAttachment = "cross-platform";
|
||||||
id = "mockCredentialId";
|
id = "mockCredentialId";
|
||||||
type = "public-key";
|
type = "public-key";
|
||||||
|
@ -1,16 +1,86 @@
|
|||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||||
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||||
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
import { UserKey } from "@bitwarden/common/types/key";
|
import { UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
import { WebAuthnLoginCredentials } from "../models/domain/login-credentials";
|
||||||
|
import { CacheData } from "../services/login-strategies/login-strategy.state";
|
||||||
|
|
||||||
import { LoginStrategy } from "./login.strategy";
|
import { LoginStrategy, LoginStrategyData } from "./login.strategy";
|
||||||
|
|
||||||
|
export class WebAuthnLoginStrategyData implements LoginStrategyData {
|
||||||
|
tokenRequest: WebAuthnLoginTokenRequest;
|
||||||
|
captchaBypassToken?: string;
|
||||||
|
credentials: WebAuthnLoginCredentials;
|
||||||
|
|
||||||
|
static fromJSON(obj: Jsonify<WebAuthnLoginStrategyData>): WebAuthnLoginStrategyData {
|
||||||
|
return Object.assign(new WebAuthnLoginStrategyData(), obj, {
|
||||||
|
tokenRequest: WebAuthnLoginTokenRequest.fromJSON(obj.tokenRequest),
|
||||||
|
credentials: WebAuthnLoginCredentials.fromJSON(obj.credentials),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class WebAuthnLoginStrategy extends LoginStrategy {
|
export class WebAuthnLoginStrategy extends LoginStrategy {
|
||||||
tokenRequest: WebAuthnLoginTokenRequest;
|
protected cache: BehaviorSubject<WebAuthnLoginStrategyData>;
|
||||||
private credentials: WebAuthnLoginCredentials;
|
|
||||||
|
constructor(
|
||||||
|
data: WebAuthnLoginStrategyData,
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
apiService: ApiService,
|
||||||
|
tokenService: TokenService,
|
||||||
|
appIdService: AppIdService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
messagingService: MessagingService,
|
||||||
|
logService: LogService,
|
||||||
|
stateService: StateService,
|
||||||
|
twoFactorService: TwoFactorService,
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
cryptoService,
|
||||||
|
apiService,
|
||||||
|
tokenService,
|
||||||
|
appIdService,
|
||||||
|
platformUtilsService,
|
||||||
|
messagingService,
|
||||||
|
logService,
|
||||||
|
stateService,
|
||||||
|
twoFactorService,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.cache = new BehaviorSubject(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async logIn(credentials: WebAuthnLoginCredentials) {
|
||||||
|
const data = new WebAuthnLoginStrategyData();
|
||||||
|
data.credentials = credentials;
|
||||||
|
data.tokenRequest = new WebAuthnLoginTokenRequest(
|
||||||
|
credentials.token,
|
||||||
|
credentials.deviceResponse,
|
||||||
|
await this.buildDeviceRequest(),
|
||||||
|
);
|
||||||
|
this.cache.next(data);
|
||||||
|
|
||||||
|
const [authResult] = await this.startLogIn();
|
||||||
|
return authResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async logInTwoFactor(): Promise<AuthResult> {
|
||||||
|
throw new Error("2FA not supported yet for WebAuthn Login.");
|
||||||
|
}
|
||||||
|
|
||||||
protected override async setMasterKey() {
|
protected override async setMasterKey() {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@ -29,15 +99,16 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||||||
if (userDecryptionOptions?.webAuthnPrfOption) {
|
if (userDecryptionOptions?.webAuthnPrfOption) {
|
||||||
const webAuthnPrfOption = idTokenResponse.userDecryptionOptions?.webAuthnPrfOption;
|
const webAuthnPrfOption = idTokenResponse.userDecryptionOptions?.webAuthnPrfOption;
|
||||||
|
|
||||||
|
const credentials = this.cache.value.credentials;
|
||||||
// confirm we still have the prf key
|
// confirm we still have the prf key
|
||||||
if (!this.credentials.prfKey) {
|
if (!credentials.prfKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// decrypt prf encrypted private key
|
// decrypt prf encrypted private key
|
||||||
const privateKey = await this.cryptoService.decryptToBytes(
|
const privateKey = await this.cryptoService.decryptToBytes(
|
||||||
webAuthnPrfOption.encryptedPrivateKey,
|
webAuthnPrfOption.encryptedPrivateKey,
|
||||||
this.credentials.prfKey,
|
credentials.prfKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
// decrypt user key with private key
|
// decrypt user key with private key
|
||||||
@ -58,22 +129,9 @@ export class WebAuthnLoginStrategy extends LoginStrategy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async logInTwoFactor(): Promise<AuthResult> {
|
exportCache(): CacheData {
|
||||||
throw new Error("2FA not supported yet for WebAuthn Login.");
|
return {
|
||||||
}
|
webAuthn: this.cache.value,
|
||||||
|
};
|
||||||
async logIn(credentials: WebAuthnLoginCredentials) {
|
|
||||||
// NOTE: To avoid DeadObject references on Firefox, do not set the credentials object directly
|
|
||||||
// Use deep copy in future if objects are added that were created in popup
|
|
||||||
this.credentials = { ...credentials };
|
|
||||||
|
|
||||||
this.tokenRequest = new WebAuthnLoginTokenRequest(
|
|
||||||
credentials.token,
|
|
||||||
credentials.deviceResponse,
|
|
||||||
await this.buildDeviceRequest(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const [authResult] = await this.startLogIn();
|
|
||||||
return authResult;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||||
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||||
@ -28,7 +30,7 @@ export class SsoLoginCredentials {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class UserApiLoginCredentials {
|
export class UserApiLoginCredentials {
|
||||||
readonly type = AuthenticationType.UserApi;
|
readonly type = AuthenticationType.UserApiKey;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public clientId: string,
|
public clientId: string,
|
||||||
@ -48,6 +50,30 @@ export class AuthRequestLoginCredentials {
|
|||||||
public decryptedMasterKeyHash: string,
|
public decryptedMasterKeyHash: string,
|
||||||
public twoFactor?: TokenTwoFactorRequest,
|
public twoFactor?: TokenTwoFactorRequest,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
static fromJSON(json: Jsonify<AuthRequestLoginCredentials>) {
|
||||||
|
return Object.assign(
|
||||||
|
new AuthRequestLoginCredentials(
|
||||||
|
json.email,
|
||||||
|
json.accessCode,
|
||||||
|
json.authRequestId,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
json.decryptedMasterKeyHash,
|
||||||
|
json.twoFactor
|
||||||
|
? new TokenTwoFactorRequest(
|
||||||
|
json.twoFactor.provider,
|
||||||
|
json.twoFactor.token,
|
||||||
|
json.twoFactor.remember,
|
||||||
|
)
|
||||||
|
: json.twoFactor,
|
||||||
|
),
|
||||||
|
{
|
||||||
|
decryptedUserKey: SymmetricCryptoKey.fromJSON(json.decryptedUserKey) as UserKey,
|
||||||
|
decryptedMasterKey: SymmetricCryptoKey.fromJSON(json.decryptedMasterKey) as MasterKey,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebAuthnLoginCredentials {
|
export class WebAuthnLoginCredentials {
|
||||||
@ -58,4 +84,15 @@ export class WebAuthnLoginCredentials {
|
|||||||
public deviceResponse: WebAuthnLoginAssertionResponseRequest,
|
public deviceResponse: WebAuthnLoginAssertionResponseRequest,
|
||||||
public prfKey?: SymmetricCryptoKey,
|
public prfKey?: SymmetricCryptoKey,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
static fromJSON(json: Jsonify<WebAuthnLoginCredentials>) {
|
||||||
|
return new WebAuthnLoginCredentials(
|
||||||
|
json.token,
|
||||||
|
Object.assign(
|
||||||
|
Object.create(WebAuthnLoginAssertionResponseRequest.prototype),
|
||||||
|
json.deviceResponse,
|
||||||
|
),
|
||||||
|
SymmetricCryptoKey.fromJSON(json.prfKey),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,201 @@
|
|||||||
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
|
import { DeviceTrustCryptoServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust-crypto.service.abstraction";
|
||||||
|
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
|
||||||
|
import { TokenService } from "@bitwarden/common/auth/abstractions/token.service";
|
||||||
|
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
|
||||||
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
|
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||||
|
import { IdentityTokenResponse } from "@bitwarden/common/auth/models/response/identity-token.response";
|
||||||
|
import { IdentityTwoFactorResponse } from "@bitwarden/common/auth/models/response/identity-two-factor.response";
|
||||||
|
import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
|
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
|
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec";
|
||||||
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
|
|
||||||
|
import { AuthRequestServiceAbstraction } from "../../abstractions";
|
||||||
|
import { PasswordLoginCredentials } from "../../models";
|
||||||
|
|
||||||
|
import { LoginStrategyService } from "./login-strategy.service";
|
||||||
|
import { CACHE_EXPIRATION_KEY } from "./login-strategy.state";
|
||||||
|
|
||||||
|
describe("LoginStrategyService", () => {
|
||||||
|
let sut: LoginStrategyService;
|
||||||
|
|
||||||
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
|
let apiService: MockProxy<ApiService>;
|
||||||
|
let tokenService: MockProxy<TokenService>;
|
||||||
|
let appIdService: MockProxy<AppIdService>;
|
||||||
|
let platformUtilsService: MockProxy<PlatformUtilsService>;
|
||||||
|
let messagingService: MockProxy<MessagingService>;
|
||||||
|
let logService: MockProxy<LogService>;
|
||||||
|
let keyConnectorService: MockProxy<KeyConnectorService>;
|
||||||
|
let environmentService: MockProxy<EnvironmentService>;
|
||||||
|
let stateService: MockProxy<StateService>;
|
||||||
|
let twoFactorService: MockProxy<TwoFactorService>;
|
||||||
|
let i18nService: MockProxy<I18nService>;
|
||||||
|
let encryptService: MockProxy<EncryptService>;
|
||||||
|
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
|
||||||
|
let policyService: MockProxy<PolicyService>;
|
||||||
|
let deviceTrustCryptoService: MockProxy<DeviceTrustCryptoServiceAbstraction>;
|
||||||
|
let authRequestService: MockProxy<AuthRequestServiceAbstraction>;
|
||||||
|
|
||||||
|
let stateProvider: FakeGlobalStateProvider;
|
||||||
|
let loginStrategyCacheExpirationState: FakeGlobalState<Date | null>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cryptoService = mock<CryptoService>();
|
||||||
|
apiService = mock<ApiService>();
|
||||||
|
tokenService = mock<TokenService>();
|
||||||
|
appIdService = mock<AppIdService>();
|
||||||
|
platformUtilsService = mock<PlatformUtilsService>();
|
||||||
|
messagingService = mock<MessagingService>();
|
||||||
|
logService = mock<LogService>();
|
||||||
|
keyConnectorService = mock<KeyConnectorService>();
|
||||||
|
environmentService = mock<EnvironmentService>();
|
||||||
|
stateService = mock<StateService>();
|
||||||
|
twoFactorService = mock<TwoFactorService>();
|
||||||
|
i18nService = mock<I18nService>();
|
||||||
|
encryptService = mock<EncryptService>();
|
||||||
|
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
|
||||||
|
policyService = mock<PolicyService>();
|
||||||
|
deviceTrustCryptoService = mock<DeviceTrustCryptoServiceAbstraction>();
|
||||||
|
authRequestService = mock<AuthRequestServiceAbstraction>();
|
||||||
|
stateProvider = new FakeGlobalStateProvider();
|
||||||
|
|
||||||
|
sut = new LoginStrategyService(
|
||||||
|
cryptoService,
|
||||||
|
apiService,
|
||||||
|
tokenService,
|
||||||
|
appIdService,
|
||||||
|
platformUtilsService,
|
||||||
|
messagingService,
|
||||||
|
logService,
|
||||||
|
keyConnectorService,
|
||||||
|
environmentService,
|
||||||
|
stateService,
|
||||||
|
twoFactorService,
|
||||||
|
i18nService,
|
||||||
|
encryptService,
|
||||||
|
passwordStrengthService,
|
||||||
|
policyService,
|
||||||
|
deviceTrustCryptoService,
|
||||||
|
authRequestService,
|
||||||
|
stateProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
loginStrategyCacheExpirationState = stateProvider.getFake(CACHE_EXPIRATION_KEY);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an AuthResult on successful login", async () => {
|
||||||
|
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(
|
||||||
|
new IdentityTokenResponse({
|
||||||
|
ForcePasswordReset: false,
|
||||||
|
Kdf: KdfType.Argon2id,
|
||||||
|
Key: "KEY",
|
||||||
|
PrivateKey: "PRIVATE_KEY",
|
||||||
|
ResetMasterPassword: false,
|
||||||
|
access_token: "ACCESS_TOKEN",
|
||||||
|
expires_in: 3600,
|
||||||
|
refresh_token: "REFRESH_TOKEN",
|
||||||
|
scope: "api offline_access",
|
||||||
|
token_type: "Bearer",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
tokenService.decodeToken.calledWith("ACCESS_TOKEN").mockResolvedValue({
|
||||||
|
sub: "USER_ID",
|
||||||
|
name: "NAME",
|
||||||
|
email: "EMAIL",
|
||||||
|
premium: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sut.logIn(credentials);
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(AuthResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an AuthResult on successful 2fa login", async () => {
|
||||||
|
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
|
||||||
|
apiService.postIdentityToken.mockResolvedValueOnce(
|
||||||
|
new IdentityTwoFactorResponse({
|
||||||
|
TwoFactorProviders: ["0"],
|
||||||
|
TwoFactorProviders2: { 0: null },
|
||||||
|
error: "invalid_grant",
|
||||||
|
error_description: "Two factor required.",
|
||||||
|
email: undefined,
|
||||||
|
ssoEmail2faSessionToken: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.logIn(credentials);
|
||||||
|
|
||||||
|
const twoFactorToken = new TokenTwoFactorRequest(
|
||||||
|
TwoFactorProviderType.Authenticator,
|
||||||
|
"TWO_FACTOR_TOKEN",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(
|
||||||
|
new IdentityTokenResponse({
|
||||||
|
ForcePasswordReset: false,
|
||||||
|
Kdf: KdfType.Argon2id,
|
||||||
|
Key: "KEY",
|
||||||
|
PrivateKey: "PRIVATE_KEY",
|
||||||
|
ResetMasterPassword: false,
|
||||||
|
access_token: "ACCESS_TOKEN",
|
||||||
|
expires_in: 3600,
|
||||||
|
refresh_token: "REFRESH_TOKEN",
|
||||||
|
scope: "api offline_access",
|
||||||
|
token_type: "Bearer",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
tokenService.decodeToken.calledWith("ACCESS_TOKEN").mockResolvedValue({
|
||||||
|
sub: "USER_ID",
|
||||||
|
name: "NAME",
|
||||||
|
email: "EMAIL",
|
||||||
|
premium: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sut.logInTwoFactor(twoFactorToken, "CAPTCHA");
|
||||||
|
|
||||||
|
expect(result).toBeInstanceOf(AuthResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clear the cache if more than 2 mins have passed since expiration date", async () => {
|
||||||
|
const credentials = new PasswordLoginCredentials("EMAIL", "MASTER_PASSWORD");
|
||||||
|
apiService.postIdentityToken.mockResolvedValue(
|
||||||
|
new IdentityTwoFactorResponse({
|
||||||
|
TwoFactorProviders: ["0"],
|
||||||
|
TwoFactorProviders2: { 0: null },
|
||||||
|
error: "invalid_grant",
|
||||||
|
error_description: "Two factor required.",
|
||||||
|
email: undefined,
|
||||||
|
ssoEmail2faSessionToken: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await sut.logIn(credentials);
|
||||||
|
|
||||||
|
loginStrategyCacheExpirationState.stateSubject.next(new Date(Date.now() - 1000 * 60 * 5));
|
||||||
|
|
||||||
|
const twoFactorToken = new TokenTwoFactorRequest(
|
||||||
|
TwoFactorProviderType.Authenticator,
|
||||||
|
"TWO_FACTOR_TOKEN",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(sut.logInTwoFactor(twoFactorToken, "CAPTCHA")).rejects.toThrow();
|
||||||
|
});
|
||||||
|
});
|
@ -1,4 +1,12 @@
|
|||||||
import { Observable, Subject } from "rxjs";
|
import {
|
||||||
|
combineLatestWith,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
firstValueFrom,
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
shareReplay,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
@ -10,6 +18,8 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-
|
|||||||
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||||
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
import { KdfConfig } from "@bitwarden/common/auth/models/domain/kdf-config";
|
||||||
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||||
|
import { PasswordlessAuthRequest } from "@bitwarden/common/auth/models/request/passwordless-auth.request";
|
||||||
|
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
||||||
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
|
import { PreloginRequest } from "@bitwarden/common/models/request/prelogin.request";
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
import { AuthRequestPushNotification } from "@bitwarden/common/models/response/notification.response";
|
||||||
@ -23,6 +33,8 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
|
||||||
import { KdfType } from "@bitwarden/common/platform/enums";
|
import { KdfType } from "@bitwarden/common/platform/enums";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
import { GlobalState, GlobalStateProvider } from "@bitwarden/common/platform/state";
|
||||||
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
|
||||||
import { MasterKey } from "@bitwarden/common/types/key";
|
import { MasterKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
@ -40,54 +52,35 @@ import {
|
|||||||
WebAuthnLoginCredentials,
|
WebAuthnLoginCredentials,
|
||||||
} from "../../models";
|
} from "../../models";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AUTH_REQUEST_PUSH_NOTIFICATION_KEY,
|
||||||
|
CURRENT_LOGIN_STRATEGY_KEY,
|
||||||
|
CacheData,
|
||||||
|
CACHE_EXPIRATION_KEY,
|
||||||
|
CACHE_KEY,
|
||||||
|
} from "./login-strategy.state";
|
||||||
|
|
||||||
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
const sessionTimeoutLength = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
||||||
get email(): string {
|
private sessionTimeout: unknown;
|
||||||
if (
|
private currentAuthnTypeState: GlobalState<AuthenticationType | null>;
|
||||||
this.logInStrategy instanceof PasswordLoginStrategy ||
|
private loginStrategyCacheState: GlobalState<CacheData | null>;
|
||||||
this.logInStrategy instanceof AuthRequestLoginStrategy ||
|
private loginStrategyCacheExpirationState: GlobalState<Date | null>;
|
||||||
this.logInStrategy instanceof SsoLoginStrategy
|
private authRequestPushNotificationState: GlobalState<string>;
|
||||||
) {
|
|
||||||
return this.logInStrategy.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
private loginStrategy$: Observable<
|
||||||
}
|
|
||||||
|
|
||||||
get masterPasswordHash(): string {
|
|
||||||
return this.logInStrategy instanceof PasswordLoginStrategy
|
|
||||||
? this.logInStrategy.masterPasswordHash
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get accessCode(): string {
|
|
||||||
return this.logInStrategy instanceof AuthRequestLoginStrategy
|
|
||||||
? this.logInStrategy.accessCode
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get authRequestId(): string {
|
|
||||||
return this.logInStrategy instanceof AuthRequestLoginStrategy
|
|
||||||
? this.logInStrategy.authRequestId
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
get ssoEmail2FaSessionToken(): string {
|
|
||||||
return this.logInStrategy instanceof SsoLoginStrategy
|
|
||||||
? this.logInStrategy.ssoEmail2FaSessionToken
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private logInStrategy:
|
|
||||||
| UserApiLoginStrategy
|
| UserApiLoginStrategy
|
||||||
| PasswordLoginStrategy
|
| PasswordLoginStrategy
|
||||||
| SsoLoginStrategy
|
| SsoLoginStrategy
|
||||||
| AuthRequestLoginStrategy
|
| AuthRequestLoginStrategy
|
||||||
| WebAuthnLoginStrategy;
|
| WebAuthnLoginStrategy
|
||||||
private sessionTimeout: any;
|
| null
|
||||||
|
>;
|
||||||
|
|
||||||
private pushNotificationSubject = new Subject<string>();
|
currentAuthType$: Observable<AuthenticationType | null>;
|
||||||
|
// TODO: move to auth request service
|
||||||
|
authRequestPushNotification$: Observable<string>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
@ -107,7 +100,71 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||||||
protected policyService: PolicyService,
|
protected policyService: PolicyService,
|
||||||
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction,
|
||||||
protected authRequestService: AuthRequestServiceAbstraction,
|
protected authRequestService: AuthRequestServiceAbstraction,
|
||||||
) {}
|
protected stateProvider: GlobalStateProvider,
|
||||||
|
) {
|
||||||
|
this.currentAuthnTypeState = this.stateProvider.get(CURRENT_LOGIN_STRATEGY_KEY);
|
||||||
|
this.loginStrategyCacheState = this.stateProvider.get(CACHE_KEY);
|
||||||
|
this.loginStrategyCacheExpirationState = this.stateProvider.get(CACHE_EXPIRATION_KEY);
|
||||||
|
this.authRequestPushNotificationState = this.stateProvider.get(
|
||||||
|
AUTH_REQUEST_PUSH_NOTIFICATION_KEY,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.currentAuthType$ = this.currentAuthnTypeState.state$;
|
||||||
|
this.authRequestPushNotification$ = this.authRequestPushNotificationState.state$.pipe(
|
||||||
|
filter((id) => id != null),
|
||||||
|
);
|
||||||
|
this.loginStrategy$ = this.currentAuthnTypeState.state$.pipe(
|
||||||
|
distinctUntilChanged(),
|
||||||
|
combineLatestWith(this.loginStrategyCacheState.state$),
|
||||||
|
this.initializeLoginStrategy.bind(this),
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEmail(): Promise<string | null> {
|
||||||
|
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||||
|
|
||||||
|
if ("email$" in strategy) {
|
||||||
|
return await firstValueFrom(strategy.email$);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMasterPasswordHash(): Promise<string | null> {
|
||||||
|
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||||
|
|
||||||
|
if ("masterKeyHash$" in strategy) {
|
||||||
|
return await firstValueFrom(strategy.masterKeyHash$);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getSsoEmail2FaSessionToken(): Promise<string | null> {
|
||||||
|
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||||
|
|
||||||
|
if ("ssoEmail2FaSessionToken$" in strategy) {
|
||||||
|
return await firstValueFrom(strategy.ssoEmail2FaSessionToken$);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAccessCode(): Promise<string | null> {
|
||||||
|
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||||
|
|
||||||
|
if ("accessCode$" in strategy) {
|
||||||
|
return await firstValueFrom(strategy.accessCode$);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthRequestId(): Promise<string | null> {
|
||||||
|
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||||
|
|
||||||
|
if ("authRequestId$" in strategy) {
|
||||||
|
return await firstValueFrom(strategy.authRequestId$);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
async logIn(
|
async logIn(
|
||||||
credentials:
|
credentials:
|
||||||
@ -117,99 +174,27 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||||||
| AuthRequestLoginCredentials
|
| AuthRequestLoginCredentials
|
||||||
| WebAuthnLoginCredentials,
|
| WebAuthnLoginCredentials,
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
this.clearState();
|
await this.clearCache();
|
||||||
|
|
||||||
let strategy:
|
await this.currentAuthnTypeState.update((_) => credentials.type);
|
||||||
| UserApiLoginStrategy
|
|
||||||
| PasswordLoginStrategy
|
|
||||||
| SsoLoginStrategy
|
|
||||||
| AuthRequestLoginStrategy
|
|
||||||
| WebAuthnLoginStrategy;
|
|
||||||
|
|
||||||
switch (credentials.type) {
|
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||||
case AuthenticationType.Password:
|
|
||||||
strategy = new PasswordLoginStrategy(
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
this.passwordStrengthService,
|
|
||||||
this.policyService,
|
|
||||||
this,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AuthenticationType.Sso:
|
|
||||||
strategy = new SsoLoginStrategy(
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
this.keyConnectorService,
|
|
||||||
this.deviceTrustCryptoService,
|
|
||||||
this.authRequestService,
|
|
||||||
this.i18nService,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AuthenticationType.UserApi:
|
|
||||||
strategy = new UserApiLoginStrategy(
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
this.environmentService,
|
|
||||||
this.keyConnectorService,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AuthenticationType.AuthRequest:
|
|
||||||
strategy = new AuthRequestLoginStrategy(
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
this.deviceTrustCryptoService,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
case AuthenticationType.WebAuthn:
|
|
||||||
strategy = new WebAuthnLoginStrategy(
|
|
||||||
this.cryptoService,
|
|
||||||
this.apiService,
|
|
||||||
this.tokenService,
|
|
||||||
this.appIdService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.messagingService,
|
|
||||||
this.logService,
|
|
||||||
this.stateService,
|
|
||||||
this.twoFactorService,
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Do not set the credentials object directly on the strategy. They are
|
// Note: We aren't passing the credentials directly to the strategy since they are
|
||||||
// created in the popup and can cause DeadObject references on Firefox.
|
// created in the popup and can cause DeadObject references on Firefox.
|
||||||
const result = await strategy.logIn(credentials as any);
|
// This is a shallow copy, but use deep copy in future if objects are added to credentials
|
||||||
|
// that were created in popup.
|
||||||
|
// If the popup uses its own instance of this service, this can be removed.
|
||||||
|
const ownedCredentials = { ...credentials };
|
||||||
|
|
||||||
if (result?.requiresTwoFactor) {
|
const result = await strategy.logIn(ownedCredentials as any);
|
||||||
this.saveState(strategy);
|
|
||||||
|
if (result != null && !result.requiresTwoFactor) {
|
||||||
|
await this.clearCache();
|
||||||
|
} else {
|
||||||
|
// Cache the strategy data so we can attempt again later with 2fa. Cache supports different contexts
|
||||||
|
await this.loginStrategyCacheState.update((_) => strategy.exportCache());
|
||||||
|
await this.startSessionTimeout();
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -219,43 +204,32 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||||||
twoFactor: TokenTwoFactorRequest,
|
twoFactor: TokenTwoFactorRequest,
|
||||||
captchaResponse: string,
|
captchaResponse: string,
|
||||||
): Promise<AuthResult> {
|
): Promise<AuthResult> {
|
||||||
if (this.logInStrategy == null) {
|
if (!(await this.isSessionValid())) {
|
||||||
throw new Error(this.i18nService.t("sessionTimeout"));
|
throw new Error(this.i18nService.t("sessionTimeout"));
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const strategy = await firstValueFrom(this.loginStrategy$);
|
||||||
const result = await this.logInStrategy.logInTwoFactor(twoFactor, captchaResponse);
|
if (strategy == null) {
|
||||||
|
throw new Error("No login strategy found.");
|
||||||
|
}
|
||||||
|
|
||||||
// Only clear state if 2FA token has been accepted, otherwise we need to be able to try again
|
try {
|
||||||
if (!result.requiresTwoFactor && !result.requiresCaptcha) {
|
const result = await strategy.logInTwoFactor(twoFactor, captchaResponse);
|
||||||
this.clearState();
|
|
||||||
|
// Only clear cache if 2FA token has been accepted, otherwise we need to be able to try again
|
||||||
|
if (result != null && !result.requiresTwoFactor && !result.requiresCaptcha) {
|
||||||
|
await this.clearCache();
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// API exceptions are okay, but if there are any unhandled client-side errors then clear state to be safe
|
// API exceptions are okay, but if there are any unhandled client-side errors then clear cache to be safe
|
||||||
if (!(e instanceof ErrorResponse)) {
|
if (!(e instanceof ErrorResponse)) {
|
||||||
this.clearState();
|
await this.clearCache();
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authingWithUserApiKey(): boolean {
|
|
||||||
return this.logInStrategy instanceof UserApiLoginStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
authingWithSso(): boolean {
|
|
||||||
return this.logInStrategy instanceof SsoLoginStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
authingWithPassword(): boolean {
|
|
||||||
return this.logInStrategy instanceof PasswordLoginStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
authingWithPasswordless(): boolean {
|
|
||||||
return this.logInStrategy instanceof AuthRequestLoginStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
|
async makePreloginKey(masterPassword: string, email: string): Promise<MasterKey> {
|
||||||
email = email.trim().toLowerCase();
|
email = email.trim().toLowerCase();
|
||||||
let kdf: KdfType = null;
|
let kdf: KdfType = null;
|
||||||
@ -278,39 +252,171 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction {
|
|||||||
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
|
return await this.cryptoService.makeMasterKey(masterPassword, email, kdf, kdfConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async authResponsePushNotification(notification: AuthRequestPushNotification): Promise<any> {
|
// TODO move to auth request service
|
||||||
this.pushNotificationSubject.next(notification.id);
|
async sendAuthRequestPushNotification(notification: AuthRequestPushNotification): Promise<void> {
|
||||||
}
|
if (notification.id != null) {
|
||||||
|
await this.authRequestPushNotificationState.update((_) => notification.id);
|
||||||
getPushNotificationObs$(): Observable<any> {
|
|
||||||
return this.pushNotificationSubject.asObservable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private saveState(
|
|
||||||
strategy:
|
|
||||||
| UserApiLoginStrategy
|
|
||||||
| PasswordLoginStrategy
|
|
||||||
| SsoLoginStrategy
|
|
||||||
| AuthRequestLoginStrategy
|
|
||||||
| WebAuthnLoginStrategy,
|
|
||||||
) {
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: move to auth request service
|
||||||
|
async passwordlessLogin(
|
||||||
|
id: string,
|
||||||
|
key: string,
|
||||||
|
requestApproved: boolean,
|
||||||
|
): Promise<AuthRequestResponse> {
|
||||||
|
const pubKey = Utils.fromB64ToArray(key);
|
||||||
|
|
||||||
|
const masterKey = await this.cryptoService.getMasterKey();
|
||||||
|
let keyToEncrypt;
|
||||||
|
let encryptedMasterKeyHash = null;
|
||||||
|
|
||||||
|
if (masterKey) {
|
||||||
|
keyToEncrypt = masterKey.encKey;
|
||||||
|
|
||||||
|
// Only encrypt the master password hash if masterKey exists as
|
||||||
|
// we won't have a masterKeyHash without a masterKey
|
||||||
|
const masterKeyHash = await this.stateService.getKeyHash();
|
||||||
|
if (masterKeyHash != null) {
|
||||||
|
encryptedMasterKeyHash = await this.cryptoService.rsaEncrypt(
|
||||||
|
Utils.fromUtf8ToArray(masterKeyHash),
|
||||||
|
pubKey,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const userKey = await this.cryptoService.getUserKey();
|
||||||
|
keyToEncrypt = userKey.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedKey = await this.cryptoService.rsaEncrypt(keyToEncrypt, pubKey);
|
||||||
|
|
||||||
|
const request = new PasswordlessAuthRequest(
|
||||||
|
encryptedKey.encryptedString,
|
||||||
|
encryptedMasterKeyHash?.encryptedString,
|
||||||
|
await this.appIdService.getAppId(),
|
||||||
|
requestApproved,
|
||||||
|
);
|
||||||
|
return await this.apiService.putAuthRequest(id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearCache(): Promise<void> {
|
||||||
|
await this.currentAuthnTypeState.update((_) => null);
|
||||||
|
await this.loginStrategyCacheState.update((_) => null);
|
||||||
|
await this.clearSessionTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startSessionTimeout(): Promise<void> {
|
||||||
|
await this.clearSessionTimeout();
|
||||||
|
await this.loginStrategyCacheExpirationState.update(
|
||||||
|
(_) => new Date(Date.now() + sessionTimeoutLength),
|
||||||
|
);
|
||||||
|
this.sessionTimeout = setTimeout(() => this.clearCache(), sessionTimeoutLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearSessionTimeout(): Promise<void> {
|
||||||
|
await this.loginStrategyCacheExpirationState.update((_) => null);
|
||||||
|
this.sessionTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isSessionValid(): Promise<boolean> {
|
||||||
|
const cache = await firstValueFrom(this.loginStrategyCacheState.state$);
|
||||||
|
if (cache == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const expiration = await firstValueFrom(this.loginStrategyCacheExpirationState.state$);
|
||||||
|
if (expiration != null && expiration < new Date()) {
|
||||||
|
await this.clearCache();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeLoginStrategy(
|
||||||
|
source: Observable<[AuthenticationType | null, CacheData | null]>,
|
||||||
|
) {
|
||||||
|
return source.pipe(
|
||||||
|
map(([strategy, data]) => {
|
||||||
|
if (strategy == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
switch (strategy) {
|
||||||
|
case AuthenticationType.Password:
|
||||||
|
return new PasswordLoginStrategy(
|
||||||
|
data?.password,
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.appIdService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.messagingService,
|
||||||
|
this.logService,
|
||||||
|
this.stateService,
|
||||||
|
this.twoFactorService,
|
||||||
|
this.passwordStrengthService,
|
||||||
|
this.policyService,
|
||||||
|
this,
|
||||||
|
);
|
||||||
|
case AuthenticationType.Sso:
|
||||||
|
return new SsoLoginStrategy(
|
||||||
|
data?.sso,
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.appIdService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.messagingService,
|
||||||
|
this.logService,
|
||||||
|
this.stateService,
|
||||||
|
this.twoFactorService,
|
||||||
|
this.keyConnectorService,
|
||||||
|
this.deviceTrustCryptoService,
|
||||||
|
this.authRequestService,
|
||||||
|
this.i18nService,
|
||||||
|
);
|
||||||
|
case AuthenticationType.UserApiKey:
|
||||||
|
return new UserApiLoginStrategy(
|
||||||
|
data?.userApiKey,
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.appIdService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.messagingService,
|
||||||
|
this.logService,
|
||||||
|
this.stateService,
|
||||||
|
this.twoFactorService,
|
||||||
|
this.environmentService,
|
||||||
|
this.keyConnectorService,
|
||||||
|
);
|
||||||
|
case AuthenticationType.AuthRequest:
|
||||||
|
return new AuthRequestLoginStrategy(
|
||||||
|
data?.authRequest,
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.appIdService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.messagingService,
|
||||||
|
this.logService,
|
||||||
|
this.stateService,
|
||||||
|
this.twoFactorService,
|
||||||
|
this.deviceTrustCryptoService,
|
||||||
|
);
|
||||||
|
case AuthenticationType.WebAuthn:
|
||||||
|
return new WebAuthnLoginStrategy(
|
||||||
|
data?.webAuthn,
|
||||||
|
this.cryptoService,
|
||||||
|
this.apiService,
|
||||||
|
this.tokenService,
|
||||||
|
this.appIdService,
|
||||||
|
this.platformUtilsService,
|
||||||
|
this.messagingService,
|
||||||
|
this.logService,
|
||||||
|
this.stateService,
|
||||||
|
this.twoFactorService,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,155 @@
|
|||||||
|
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
|
||||||
|
import { DeviceRequest } from "@bitwarden/common/auth/models/request/identity-token/device.request";
|
||||||
|
import { PasswordTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/password-token.request";
|
||||||
|
import { SsoTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/sso-token.request";
|
||||||
|
import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request";
|
||||||
|
import { UserApiTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/user-api-token.request";
|
||||||
|
import { WebAuthnLoginTokenRequest } from "@bitwarden/common/auth/models/request/identity-token/webauthn-login-token.request";
|
||||||
|
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||||
|
import { DeviceType } from "@bitwarden/common/enums";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
|
||||||
|
import { MasterKey, PrfKey, UserKey } from "@bitwarden/common/types/key";
|
||||||
|
|
||||||
|
import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-login.strategy";
|
||||||
|
import { PasswordLoginStrategyData } from "../../login-strategies/password-login.strategy";
|
||||||
|
import { SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy";
|
||||||
|
import { UserApiLoginStrategyData } from "../../login-strategies/user-api-login.strategy";
|
||||||
|
import { WebAuthnLoginStrategyData } from "../../login-strategies/webauthn-login.strategy";
|
||||||
|
import {
|
||||||
|
MockAuthenticatorAssertionResponse,
|
||||||
|
MockPublicKeyCredential,
|
||||||
|
} from "../../login-strategies/webauthn-login.strategy.spec";
|
||||||
|
import { AuthRequestLoginCredentials, WebAuthnLoginCredentials } from "../../models";
|
||||||
|
|
||||||
|
import { CACHE_KEY } from "./login-strategy.state";
|
||||||
|
|
||||||
|
describe("LOGIN_STRATEGY_CACHE_KEY", () => {
|
||||||
|
const sut = CACHE_KEY;
|
||||||
|
|
||||||
|
let deviceRequest: DeviceRequest;
|
||||||
|
let twoFactorRequest: TokenTwoFactorRequest;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
deviceRequest = Object.assign(Object.create(DeviceRequest.prototype), {
|
||||||
|
type: DeviceType.ChromeBrowser,
|
||||||
|
name: "DEVICE_NAME",
|
||||||
|
identifier: "DEVICE_IDENTIFIER",
|
||||||
|
pushToken: "PUSH_TOKEN",
|
||||||
|
});
|
||||||
|
|
||||||
|
twoFactorRequest = new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly deserialize PasswordLoginStrategyData", () => {
|
||||||
|
const actual = {
|
||||||
|
password: new PasswordLoginStrategyData(),
|
||||||
|
};
|
||||||
|
actual.password.tokenRequest = new PasswordTokenRequest(
|
||||||
|
"EMAIL",
|
||||||
|
"LOCAL_PASSWORD_HASH",
|
||||||
|
"CAPTCHA_TOKEN",
|
||||||
|
twoFactorRequest,
|
||||||
|
deviceRequest,
|
||||||
|
);
|
||||||
|
actual.password.masterKey = new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey;
|
||||||
|
actual.password.localMasterKeyHash = "LOCAL_MASTER_KEY_HASH";
|
||||||
|
|
||||||
|
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||||
|
|
||||||
|
expect(result.password).toBeInstanceOf(PasswordLoginStrategyData);
|
||||||
|
verifyPropertyPrototypes(result, actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly deserialize SsoLoginStrategyData", () => {
|
||||||
|
const actual = { sso: new SsoLoginStrategyData() };
|
||||||
|
actual.sso.tokenRequest = new SsoTokenRequest(
|
||||||
|
"CODE",
|
||||||
|
"CODE_VERIFIER",
|
||||||
|
"REDIRECT_URI",
|
||||||
|
twoFactorRequest,
|
||||||
|
deviceRequest,
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||||
|
|
||||||
|
expect(result.sso).toBeInstanceOf(SsoLoginStrategyData);
|
||||||
|
verifyPropertyPrototypes(result, actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly deserialize UserApiLoginStrategyData", () => {
|
||||||
|
const actual = { userApiKey: new UserApiLoginStrategyData() };
|
||||||
|
actual.userApiKey.tokenRequest = new UserApiTokenRequest("CLIENT_ID", "CLIENT_SECRET", null);
|
||||||
|
|
||||||
|
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||||
|
|
||||||
|
expect(result.userApiKey).toBeInstanceOf(UserApiLoginStrategyData);
|
||||||
|
verifyPropertyPrototypes(result, actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly deserialize AuthRequestLoginStrategyData", () => {
|
||||||
|
const actual = { authRequest: new AuthRequestLoginStrategyData() };
|
||||||
|
actual.authRequest.tokenRequest = new PasswordTokenRequest("EMAIL", "ACCESS_CODE", null, null);
|
||||||
|
actual.authRequest.authRequestCredentials = new AuthRequestLoginCredentials(
|
||||||
|
"EMAIL",
|
||||||
|
"ACCESS_CODE",
|
||||||
|
"AUTH_REQUEST_ID",
|
||||||
|
new SymmetricCryptoKey(new Uint8Array(64)) as UserKey,
|
||||||
|
new SymmetricCryptoKey(new Uint8Array(64)) as MasterKey,
|
||||||
|
"MASTER_KEY_HASH",
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||||
|
|
||||||
|
expect(result.authRequest).toBeInstanceOf(AuthRequestLoginStrategyData);
|
||||||
|
verifyPropertyPrototypes(result, actual);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should correctly deserialize WebAuthnLoginStrategyData", () => {
|
||||||
|
global.AuthenticatorAssertionResponse = MockAuthenticatorAssertionResponse;
|
||||||
|
const actual = { webAuthn: new WebAuthnLoginStrategyData() };
|
||||||
|
const publicKeyCredential = new MockPublicKeyCredential();
|
||||||
|
const deviceResponse = new WebAuthnLoginAssertionResponseRequest(publicKeyCredential);
|
||||||
|
const prfKey = new SymmetricCryptoKey(new Uint8Array(64)) as PrfKey;
|
||||||
|
actual.webAuthn.credentials = new WebAuthnLoginCredentials("TOKEN", deviceResponse, prfKey);
|
||||||
|
actual.webAuthn.tokenRequest = new WebAuthnLoginTokenRequest(
|
||||||
|
"TOKEN",
|
||||||
|
deviceResponse,
|
||||||
|
deviceRequest,
|
||||||
|
);
|
||||||
|
actual.webAuthn.captchaBypassToken = "CAPTCHA_BYPASS_TOKEN";
|
||||||
|
actual.webAuthn.tokenRequest.setTwoFactor(
|
||||||
|
new TokenTwoFactorRequest(TwoFactorProviderType.Email, "TOKEN", false),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = sut.deserializer(JSON.parse(JSON.stringify(actual)));
|
||||||
|
|
||||||
|
expect(result.webAuthn).toBeInstanceOf(WebAuthnLoginStrategyData);
|
||||||
|
verifyPropertyPrototypes(result, actual);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively verifies the prototypes of all objects in the deserialized object.
|
||||||
|
* It is important that the concrete object has the correct prototypes for
|
||||||
|
* comparison.
|
||||||
|
* @param deserialized the deserialized object
|
||||||
|
* @param concrete the object stored in state
|
||||||
|
*/
|
||||||
|
function verifyPropertyPrototypes(deserialized: object, concrete: object) {
|
||||||
|
for (const key of Object.keys(deserialized)) {
|
||||||
|
const deserializedProperty = (deserialized as any)[key];
|
||||||
|
if (deserializedProperty === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const realProperty = (concrete as any)[key];
|
||||||
|
if (realProperty === undefined) {
|
||||||
|
throw new Error(`Expected ${key} to be defined in ${concrete.constructor.name}`);
|
||||||
|
}
|
||||||
|
// we only care about checking prototypes of objects
|
||||||
|
if (typeof realProperty === "object" && realProperty !== null) {
|
||||||
|
const realProto = Object.getPrototypeOf(realProperty);
|
||||||
|
expect(deserializedProperty).toBeInstanceOf(realProto.constructor);
|
||||||
|
verifyPropertyPrototypes(deserializedProperty, realProperty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,80 @@
|
|||||||
|
import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication-type";
|
||||||
|
import { KeyDefinition, LOGIN_STRATEGY_MEMORY } from "@bitwarden/common/platform/state";
|
||||||
|
|
||||||
|
import { AuthRequestLoginStrategyData } from "../../login-strategies/auth-request-login.strategy";
|
||||||
|
import { PasswordLoginStrategyData } from "../../login-strategies/password-login.strategy";
|
||||||
|
import { SsoLoginStrategyData } from "../../login-strategies/sso-login.strategy";
|
||||||
|
import { UserApiLoginStrategyData } from "../../login-strategies/user-api-login.strategy";
|
||||||
|
import { WebAuthnLoginStrategyData } from "../../login-strategies/webauthn-login.strategy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current login strategy in use.
|
||||||
|
*/
|
||||||
|
export const CURRENT_LOGIN_STRATEGY_KEY = new KeyDefinition<AuthenticationType | null>(
|
||||||
|
LOGIN_STRATEGY_MEMORY,
|
||||||
|
"currentLoginStrategy",
|
||||||
|
{
|
||||||
|
deserializer: (data) => data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The expiration date for the login strategy cache.
|
||||||
|
* Used as a backup to the timer set on the service.
|
||||||
|
*/
|
||||||
|
export const CACHE_EXPIRATION_KEY = new KeyDefinition<Date | null>(
|
||||||
|
LOGIN_STRATEGY_MEMORY,
|
||||||
|
"loginStrategyCacheExpiration",
|
||||||
|
{
|
||||||
|
deserializer: (data) => (data ? null : new Date(data)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auth Request notification for all instances of the login strategy service.
|
||||||
|
* Note: this isn't an ideal approach, but allows both a background and
|
||||||
|
* foreground instance to send out the notification.
|
||||||
|
* TODO: Move to Auth Request service.
|
||||||
|
*/
|
||||||
|
export const AUTH_REQUEST_PUSH_NOTIFICATION_KEY = new KeyDefinition<string>(
|
||||||
|
LOGIN_STRATEGY_MEMORY,
|
||||||
|
"authRequestPushNotification",
|
||||||
|
{
|
||||||
|
deserializer: (data) => data,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export type CacheData = {
|
||||||
|
password?: PasswordLoginStrategyData;
|
||||||
|
sso?: SsoLoginStrategyData;
|
||||||
|
userApiKey?: UserApiLoginStrategyData;
|
||||||
|
authRequest?: AuthRequestLoginStrategyData;
|
||||||
|
webAuthn?: WebAuthnLoginStrategyData;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache for login strategies to use for data persistence through
|
||||||
|
* the login process.
|
||||||
|
*/
|
||||||
|
export const CACHE_KEY = new KeyDefinition<CacheData | null>(
|
||||||
|
LOGIN_STRATEGY_MEMORY,
|
||||||
|
"loginStrategyCache",
|
||||||
|
{
|
||||||
|
deserializer: (data) => {
|
||||||
|
if (data == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
password: data.password ? PasswordLoginStrategyData.fromJSON(data.password) : undefined,
|
||||||
|
sso: data.sso ? SsoLoginStrategyData.fromJSON(data.sso) : undefined,
|
||||||
|
userApiKey: data.userApiKey
|
||||||
|
? UserApiLoginStrategyData.fromJSON(data.userApiKey)
|
||||||
|
: undefined,
|
||||||
|
authRequest: data.authRequest
|
||||||
|
? AuthRequestLoginStrategyData.fromJSON(data.authRequest)
|
||||||
|
: undefined,
|
||||||
|
webAuthn: data.webAuthn ? WebAuthnLoginStrategyData.fromJSON(data.webAuthn) : undefined,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
@ -1,14 +1,8 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { AccountSettingsSettings } from "../platform/models/domain/account";
|
|
||||||
|
|
||||||
export abstract class SettingsService {
|
export abstract class SettingsService {
|
||||||
settings$: Observable<AccountSettingsSettings>;
|
|
||||||
disableFavicon$: Observable<boolean>;
|
disableFavicon$: Observable<boolean>;
|
||||||
|
|
||||||
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
|
|
||||||
getEquivalentDomains: (url: string) => Set<string>;
|
|
||||||
setDisableFavicon: (value: boolean) => Promise<any>;
|
setDisableFavicon: (value: boolean) => Promise<any>;
|
||||||
getDisableFavicon: () => boolean;
|
getDisableFavicon: () => boolean;
|
||||||
clear: (userId?: string) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
export enum AuthenticationType {
|
export enum AuthenticationType {
|
||||||
Password = 0,
|
Password = 0,
|
||||||
Sso = 1,
|
Sso = 1,
|
||||||
UserApi = 2,
|
UserApiKey = 2,
|
||||||
AuthRequest = 3,
|
AuthRequest = 3,
|
||||||
WebAuthn = 4,
|
WebAuthn = 4,
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { DeviceType } from "../../../../enums";
|
import { DeviceType } from "../../../../enums";
|
||||||
import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "../../../../platform/abstractions/platform-utils.service";
|
||||||
|
|
||||||
@ -13,4 +15,8 @@ export class DeviceRequest {
|
|||||||
this.identifier = appId;
|
this.identifier = appId;
|
||||||
this.pushToken = null;
|
this.pushToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: Jsonify<DeviceRequest>) {
|
||||||
|
return Object.assign(Object.create(DeviceRequest.prototype), json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,4 +34,13 @@ export class PasswordTokenRequest extends TokenRequest implements CaptchaProtect
|
|||||||
alterIdentityTokenHeaders(headers: Headers) {
|
alterIdentityTokenHeaders(headers: Headers) {
|
||||||
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
|
headers.set("Auth-Email", Utils.fromUtf8ToUrlB64(this.email));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: any) {
|
||||||
|
return Object.assign(Object.create(PasswordTokenRequest.prototype), json, {
|
||||||
|
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||||
|
twoFactor: json.twoFactor
|
||||||
|
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -23,4 +23,13 @@ export class SsoTokenRequest extends TokenRequest {
|
|||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: any) {
|
||||||
|
return Object.assign(Object.create(SsoTokenRequest.prototype), json, {
|
||||||
|
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||||
|
twoFactor: json.twoFactor
|
||||||
|
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,4 +21,13 @@ export class UserApiTokenRequest extends TokenRequest {
|
|||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: any) {
|
||||||
|
return Object.assign(Object.create(UserApiTokenRequest.prototype), json, {
|
||||||
|
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||||
|
twoFactor: json.twoFactor
|
||||||
|
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request";
|
import { WebAuthnLoginAssertionResponseRequest } from "../../../services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||||
|
|
||||||
import { DeviceRequest } from "./device.request";
|
import { DeviceRequest } from "./device.request";
|
||||||
|
import { TokenTwoFactorRequest } from "./token-two-factor.request";
|
||||||
import { TokenRequest } from "./token.request";
|
import { TokenRequest } from "./token.request";
|
||||||
|
|
||||||
export class WebAuthnLoginTokenRequest extends TokenRequest {
|
export class WebAuthnLoginTokenRequest extends TokenRequest {
|
||||||
@ -22,4 +23,14 @@ export class WebAuthnLoginTokenRequest extends TokenRequest {
|
|||||||
|
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: any) {
|
||||||
|
return Object.assign(Object.create(WebAuthnLoginTokenRequest.prototype), json, {
|
||||||
|
deviceResponse: WebAuthnLoginAssertionResponseRequest.fromJSON(json.deviceResponse),
|
||||||
|
device: json.device ? DeviceRequest.fromJSON(json.device) : undefined,
|
||||||
|
twoFactor: json.twoFactor
|
||||||
|
? Object.assign(new TokenTwoFactorRequest(), json.twoFactor)
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ export class AnonymousHubService implements AnonymousHubServiceAbstraction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async ProcessNotification(notification: NotificationResponse) {
|
private async ProcessNotification(notification: NotificationResponse) {
|
||||||
await this.loginStrategyService.authResponsePushNotification(
|
await this.loginStrategyService.sendAuthRequestPushNotification(
|
||||||
notification.payload as AuthRequestPushNotification,
|
notification.payload as AuthRequestPushNotification,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
import { mock, MockProxy } from "jest-mock-extended";
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
|
import { BehaviorSubject } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../../../common/src/types/guid";
|
||||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
|
||||||
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
|
import { OrganizationAutoEnrollStatusResponse } from "../../admin-console/models/response/organization-auto-enroll-status.response";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { AccountInfo, AccountService } from "../abstractions/account.service";
|
||||||
|
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||||
|
|
||||||
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
import { PasswordResetEnrollmentServiceImplementation } from "./password-reset-enrollment.service.implementation";
|
||||||
|
|
||||||
describe("PasswordResetEnrollmentServiceImplementation", () => {
|
describe("PasswordResetEnrollmentServiceImplementation", () => {
|
||||||
|
const activeAccountSubject = new BehaviorSubject<{ id: UserId } & AccountInfo>(null);
|
||||||
|
|
||||||
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
let organizationApiService: MockProxy<OrganizationApiServiceAbstraction>;
|
||||||
let stateService: MockProxy<StateService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let organizationUserService: MockProxy<OrganizationUserService>;
|
let organizationUserService: MockProxy<OrganizationUserService>;
|
||||||
let i18nService: MockProxy<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
@ -19,13 +24,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
organizationApiService = mock<OrganizationApiServiceAbstraction>();
|
||||||
stateService = mock<StateService>();
|
accountService = mock<AccountService>();
|
||||||
|
accountService.activeAccount$ = activeAccountSubject;
|
||||||
cryptoService = mock<CryptoService>();
|
cryptoService = mock<CryptoService>();
|
||||||
organizationUserService = mock<OrganizationUserService>();
|
organizationUserService = mock<OrganizationUserService>();
|
||||||
i18nService = mock<I18nService>();
|
i18nService = mock<I18nService>();
|
||||||
service = new PasswordResetEnrollmentServiceImplementation(
|
service = new PasswordResetEnrollmentServiceImplementation(
|
||||||
organizationApiService,
|
organizationApiService,
|
||||||
stateService,
|
accountService,
|
||||||
cryptoService,
|
cryptoService,
|
||||||
organizationUserService,
|
organizationUserService,
|
||||||
i18nService,
|
i18nService,
|
||||||
@ -81,7 +87,14 @@ describe("PasswordResetEnrollmentServiceImplementation", () => {
|
|||||||
};
|
};
|
||||||
const encryptedKey = { encryptedString: "encryptedString" };
|
const encryptedKey = { encryptedString: "encryptedString" };
|
||||||
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
organizationApiService.getKeys.mockResolvedValue(orgKeyResponse as any);
|
||||||
stateService.getUserId.mockResolvedValue("userId");
|
|
||||||
|
const user1AccountInfo: AccountInfo = {
|
||||||
|
name: "Test User 1",
|
||||||
|
email: "test1@email.com",
|
||||||
|
status: AuthenticationStatus.Unlocked,
|
||||||
|
};
|
||||||
|
activeAccountSubject.next(Object.assign(user1AccountInfo, { id: "userId" as UserId }));
|
||||||
|
|
||||||
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
|
cryptoService.getUserKey.mockResolvedValue({ key: "key" } as any);
|
||||||
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
cryptoService.rsaEncrypt.mockResolvedValue(encryptedKey as any);
|
||||||
|
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
|
import { firstValueFrom, map } from "rxjs";
|
||||||
|
|
||||||
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
import { OrganizationApiServiceAbstraction } from "../../admin-console/abstractions/organization/organization-api.service.abstraction";
|
||||||
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "../../admin-console/abstractions/organization-user/organization-user.service";
|
||||||
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../admin-console/abstractions/organization-user/requests";
|
import { OrganizationUserResetPasswordEnrollmentRequest } from "../../admin-console/abstractions/organization-user/requests";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
import { UserKey } from "../../types/key";
|
import { UserKey } from "../../types/key";
|
||||||
|
import { AccountService } from "../abstractions/account.service";
|
||||||
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
|
import { PasswordResetEnrollmentServiceAbstraction } from "../abstractions/password-reset-enrollment.service.abstraction";
|
||||||
|
|
||||||
export class PasswordResetEnrollmentServiceImplementation
|
export class PasswordResetEnrollmentServiceImplementation
|
||||||
@ -13,7 +15,7 @@ export class PasswordResetEnrollmentServiceImplementation
|
|||||||
{
|
{
|
||||||
constructor(
|
constructor(
|
||||||
protected organizationApiService: OrganizationApiServiceAbstraction,
|
protected organizationApiService: OrganizationApiServiceAbstraction,
|
||||||
protected stateService: StateService,
|
protected accountService: AccountService,
|
||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
protected organizationUserService: OrganizationUserService,
|
protected organizationUserService: OrganizationUserService,
|
||||||
protected i18nService: I18nService,
|
protected i18nService: I18nService,
|
||||||
@ -38,7 +40,8 @@ export class PasswordResetEnrollmentServiceImplementation
|
|||||||
|
|
||||||
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
|
const orgPublicKey = Utils.fromB64ToArray(orgKeyResponse.publicKey);
|
||||||
|
|
||||||
userId = userId ?? (await this.stateService.getUserId());
|
userId =
|
||||||
|
userId ?? (await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))));
|
||||||
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
|
userKey = userKey ?? (await this.cryptoService.getUserKey(userId));
|
||||||
// RSA Encrypt user's userKey.key with organization public key
|
// RSA Encrypt user's userKey.key with organization public key
|
||||||
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);
|
const encryptedKey = await this.cryptoService.rsaEncrypt(userKey.key, orgPublicKey);
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { Utils } from "../../../../platform/misc/utils";
|
import { Utils } from "../../../../platform/misc/utils";
|
||||||
|
|
||||||
import { WebAuthnLoginResponseRequest } from "./webauthn-login-response.request";
|
import { WebAuthnLoginResponseRequest } from "./webauthn-login-response.request";
|
||||||
@ -27,4 +29,8 @@ export class WebAuthnLoginAssertionResponseRequest extends WebAuthnLoginResponse
|
|||||||
userHandle: Utils.fromBufferToUrlB64(credential.response.userHandle),
|
userHandle: Utils.fromBufferToUrlB64(credential.response.userHandle),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static fromJSON(json: Jsonify<WebAuthnLoginAssertionResponseRequest>) {
|
||||||
|
return Object.assign(Object.create(WebAuthnLoginAssertionResponseRequest.prototype), json);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,53 @@
|
|||||||
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
|
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
|
|
||||||
|
import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-settings.service";
|
||||||
|
|
||||||
|
describe("DefaultDomainSettingsService", () => {
|
||||||
|
let domainSettingsService: DomainSettingsService;
|
||||||
|
const mockUserId = Utils.newGuid() as UserId;
|
||||||
|
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||||
|
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
||||||
|
|
||||||
|
const mockEquivalentDomains = [
|
||||||
|
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
|
||||||
|
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
|
||||||
|
["example.co.uk", "exampleapp.co.uk"],
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||||
|
|
||||||
|
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
|
||||||
|
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getUrlEquivalentDomains", () => {
|
||||||
|
it("returns all equivalent domains for a URL", async () => {
|
||||||
|
const expected = new Set([
|
||||||
|
"example.com",
|
||||||
|
"exampleapp.com",
|
||||||
|
"example.co.uk",
|
||||||
|
"ejemplo.es",
|
||||||
|
"exampleapp.co.uk",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const actual = await firstValueFrom(
|
||||||
|
domainSettingsService.getUrlEquivalentDomains("example.co.uk"),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("example.co.uk");
|
||||||
|
expect(actual).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns an empty set if there are no equivalent domains", async () => {
|
||||||
|
const actual = await firstValueFrom(domainSettingsService.getUrlEquivalentDomains("asdf"));
|
||||||
|
|
||||||
|
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("asdf");
|
||||||
|
expect(actual).toEqual(new Set());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
97
libs/common/src/autofill/services/domain-settings.service.ts
Normal file
97
libs/common/src/autofill/services/domain-settings.service.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { map, Observable } from "rxjs";
|
||||||
|
|
||||||
|
import {
|
||||||
|
NeverDomains,
|
||||||
|
EquivalentDomains,
|
||||||
|
UriMatchStrategySetting,
|
||||||
|
UriMatchStrategy,
|
||||||
|
} from "../../models/domain/domain-service";
|
||||||
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import {
|
||||||
|
DOMAIN_SETTINGS_DISK,
|
||||||
|
ActiveUserState,
|
||||||
|
GlobalState,
|
||||||
|
KeyDefinition,
|
||||||
|
StateProvider,
|
||||||
|
UserKeyDefinition,
|
||||||
|
} from "../../platform/state";
|
||||||
|
|
||||||
|
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
|
||||||
|
deserializer: (value: NeverDomains) => value ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
|
||||||
|
deserializer: (value: EquivalentDomains) => value ?? null,
|
||||||
|
clearOn: ["logout"],
|
||||||
|
});
|
||||||
|
|
||||||
|
const DEFAULT_URI_MATCH_STRATEGY = new KeyDefinition(
|
||||||
|
DOMAIN_SETTINGS_DISK,
|
||||||
|
"defaultUriMatchStrategy",
|
||||||
|
{
|
||||||
|
deserializer: (value: UriMatchStrategySetting) => value ?? UriMatchStrategy.Domain,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export abstract class DomainSettingsService {
|
||||||
|
neverDomains$: Observable<NeverDomains>;
|
||||||
|
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
|
||||||
|
equivalentDomains$: Observable<EquivalentDomains>;
|
||||||
|
setEquivalentDomains: (newValue: EquivalentDomains) => Promise<void>;
|
||||||
|
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||||
|
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
|
||||||
|
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DefaultDomainSettingsService implements DomainSettingsService {
|
||||||
|
private neverDomainsState: GlobalState<NeverDomains>;
|
||||||
|
readonly neverDomains$: Observable<NeverDomains>;
|
||||||
|
|
||||||
|
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
|
||||||
|
readonly equivalentDomains$: Observable<EquivalentDomains>;
|
||||||
|
|
||||||
|
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
|
||||||
|
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||||
|
|
||||||
|
constructor(private stateProvider: StateProvider) {
|
||||||
|
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
|
||||||
|
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
|
||||||
|
|
||||||
|
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
|
||||||
|
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
|
||||||
|
|
||||||
|
this.defaultUriMatchStrategyState = this.stateProvider.getActive(DEFAULT_URI_MATCH_STRATEGY);
|
||||||
|
this.defaultUriMatchStrategy$ = this.defaultUriMatchStrategyState.state$.pipe(
|
||||||
|
map((x) => x ?? UriMatchStrategy.Domain),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setNeverDomains(newValue: NeverDomains): Promise<void> {
|
||||||
|
await this.neverDomainsState.update(() => newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setEquivalentDomains(newValue: EquivalentDomains): Promise<void> {
|
||||||
|
await this.equivalentDomainsState.update(() => newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise<void> {
|
||||||
|
await this.defaultUriMatchStrategyState.update(() => newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrlEquivalentDomains(url: string): Observable<Set<string>> {
|
||||||
|
const domains$ = this.equivalentDomains$.pipe(
|
||||||
|
map((equivalentDomains) => {
|
||||||
|
const domain = Utils.getDomain(url);
|
||||||
|
if (domain == null || equivalentDomains == null) {
|
||||||
|
return new Set() as Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const equivalents = equivalentDomains.filter((ed) => ed.includes(domain)).flat();
|
||||||
|
|
||||||
|
return new Set(equivalents);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return domains$;
|
||||||
|
}
|
||||||
|
}
|
24
libs/common/src/models/domain/domain-service.ts
Normal file
24
libs/common/src/models/domain/domain-service.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
See full documentation at:
|
||||||
|
https://bitwarden.com/help/uri-match-detection/#match-detection-options
|
||||||
|
|
||||||
|
Domain: "the top-level domain and second-level domain of the URI match the detected resource",
|
||||||
|
Host: "the hostname and (if specified) port of the URI matches the detected resource",
|
||||||
|
StartsWith: "the detected resource starts with the URI, regardless of what follows it",
|
||||||
|
Exact: "the URI matches the detected resource exactly",
|
||||||
|
RegularExpression: "the detected resource matches a specified regular expression",
|
||||||
|
Never: "never offer auto-fill for the item",
|
||||||
|
*/
|
||||||
|
export const UriMatchStrategy = {
|
||||||
|
Domain: 0,
|
||||||
|
Host: 1,
|
||||||
|
StartsWith: 2,
|
||||||
|
Exact: 3,
|
||||||
|
RegularExpression: 4,
|
||||||
|
Never: 5,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
|
||||||
|
|
||||||
|
export type NeverDomains = { [id: string]: unknown };
|
||||||
|
export type EquivalentDomains = string[][];
|
@ -1,5 +1,5 @@
|
|||||||
|
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
|
||||||
import { EncString } from "../../platform/models/domain/enc-string";
|
import { EncString } from "../../platform/models/domain/enc-string";
|
||||||
import { UriMatchType } from "../../vault/enums";
|
|
||||||
import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri";
|
import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri";
|
||||||
import { LoginUriView } from "../../vault/models/view/login-uri.view";
|
import { LoginUriView } from "../../vault/models/view/login-uri.view";
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ export class LoginUriExport {
|
|||||||
|
|
||||||
uri: string;
|
uri: string;
|
||||||
uriChecksum: string | undefined;
|
uriChecksum: string | undefined;
|
||||||
match: UriMatchType = null;
|
match: UriMatchStrategySetting = null;
|
||||||
|
|
||||||
constructor(o?: LoginUriView | LoginUriDomain) {
|
constructor(o?: LoginUriView | LoginUriDomain) {
|
||||||
if (o == null) {
|
if (o == null) {
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
export abstract class AppIdService {
|
export abstract class AppIdService {
|
||||||
|
appId$: Observable<string>;
|
||||||
|
anonymousAppId$: Observable<string>;
|
||||||
getAppId: () => Promise<string>;
|
getAppId: () => Promise<string>;
|
||||||
getAnonymousAppId: () => Promise<string>;
|
getAnonymousAppId: () => Promise<string>;
|
||||||
}
|
}
|
||||||
|
@ -14,17 +14,12 @@ import { SendData } from "../../tools/send/models/data/send.data";
|
|||||||
import { SendView } from "../../tools/send/models/view/send.view";
|
import { SendView } from "../../tools/send/models/view/send.view";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, MasterKey } from "../../types/key";
|
import { DeviceKey, MasterKey } from "../../types/key";
|
||||||
import { UriMatchType } from "../../vault/enums";
|
|
||||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
import { LocalData } from "../../vault/models/data/local.data";
|
import { LocalData } from "../../vault/models/data/local.data";
|
||||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||||
import { KdfType, ThemeType } from "../enums";
|
import { KdfType, ThemeType } from "../enums";
|
||||||
import { ServerConfigData } from "../models/data/server-config.data";
|
import { ServerConfigData } from "../models/data/server-config.data";
|
||||||
import {
|
import { Account, AccountDecryptionOptions } from "../models/domain/account";
|
||||||
Account,
|
|
||||||
AccountDecryptionOptions,
|
|
||||||
AccountSettingsSettings,
|
|
||||||
} from "../models/domain/account";
|
|
||||||
import { EncString } from "../models/domain/enc-string";
|
import { EncString } from "../models/domain/enc-string";
|
||||||
import { StorageOptions } from "../models/domain/storage-options";
|
import { StorageOptions } from "../models/domain/storage-options";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||||
@ -184,8 +179,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
* @deprecated Do not call this directly, use SendService
|
* @deprecated Do not call this directly, use SendService
|
||||||
*/
|
*/
|
||||||
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
|
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
|
||||||
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
|
|
||||||
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Do not call this, use SettingsService
|
* @deprecated Do not call this, use SettingsService
|
||||||
*/
|
*/
|
||||||
@ -272,8 +265,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
* @deprecated Do not call this directly, use SendService
|
* @deprecated Do not call this directly, use SendService
|
||||||
*/
|
*/
|
||||||
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
|
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
|
||||||
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
|
|
||||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
|
||||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
||||||
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
|
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
|
||||||
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
||||||
@ -307,8 +298,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
|
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
|
||||||
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>;
|
|
||||||
setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise<void>;
|
|
||||||
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
|
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
|
||||||
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||||
@ -350,14 +339,6 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
/**
|
|
||||||
* @deprecated Do not call this directly, use SettingsService
|
|
||||||
*/
|
|
||||||
getSettings: (options?: StorageOptions) => Promise<AccountSettingsSettings>;
|
|
||||||
/**
|
|
||||||
* @deprecated Do not call this directly, use SettingsService
|
|
||||||
*/
|
|
||||||
setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise<void>;
|
|
||||||
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
||||||
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
||||||
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
||||||
|
@ -253,11 +253,10 @@ export class Utils {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
||||||
|
|
||||||
static isGuid(id: string) {
|
static isGuid(id: string) {
|
||||||
return RegExp(
|
return RegExp(Utils.guidRegex, "i").test(id);
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
|
|
||||||
"i",
|
|
||||||
).test(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static getHostname(uriString: string): string {
|
static getHostname(uriString: string): string {
|
||||||
|
@ -7,6 +7,7 @@ import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/us
|
|||||||
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
|
||||||
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
|
||||||
import { EventData } from "../../../models/data/event.data";
|
import { EventData } from "../../../models/data/event.data";
|
||||||
|
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
|
||||||
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
import { GeneratorOptions } from "../../../tools/generator/generator-options";
|
||||||
import {
|
import {
|
||||||
GeneratedPasswordHistory,
|
GeneratedPasswordHistory,
|
||||||
@ -17,7 +18,6 @@ import { SendData } from "../../../tools/send/models/data/send.data";
|
|||||||
import { SendView } from "../../../tools/send/models/view/send.view";
|
import { SendView } from "../../../tools/send/models/view/send.view";
|
||||||
import { DeepJsonify } from "../../../types/deep-jsonify";
|
import { DeepJsonify } from "../../../types/deep-jsonify";
|
||||||
import { MasterKey } from "../../../types/key";
|
import { MasterKey } from "../../../types/key";
|
||||||
import { UriMatchType } from "../../../vault/enums";
|
|
||||||
import { CipherData } from "../../../vault/models/data/cipher.data";
|
import { CipherData } from "../../../vault/models/data/cipher.data";
|
||||||
import { CipherView } from "../../../vault/models/view/cipher.view";
|
import { CipherView } from "../../../vault/models/view/cipher.view";
|
||||||
import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info";
|
import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info";
|
||||||
@ -196,13 +196,12 @@ export class AccountProfile {
|
|||||||
|
|
||||||
export class AccountSettings {
|
export class AccountSettings {
|
||||||
autoConfirmFingerPrints?: boolean;
|
autoConfirmFingerPrints?: boolean;
|
||||||
defaultUriMatch?: UriMatchType;
|
defaultUriMatch?: UriMatchStrategySetting;
|
||||||
disableGa?: boolean;
|
disableGa?: boolean;
|
||||||
dontShowCardsCurrentTab?: boolean;
|
dontShowCardsCurrentTab?: boolean;
|
||||||
dontShowIdentitiesCurrentTab?: boolean;
|
dontShowIdentitiesCurrentTab?: boolean;
|
||||||
enableAlwaysOnTop?: boolean;
|
enableAlwaysOnTop?: boolean;
|
||||||
enableBiometric?: boolean;
|
enableBiometric?: boolean;
|
||||||
equivalentDomains?: any;
|
|
||||||
minimizeOnCopyToClipboard?: boolean;
|
minimizeOnCopyToClipboard?: boolean;
|
||||||
passwordGenerationOptions?: PasswordGeneratorOptions;
|
passwordGenerationOptions?: PasswordGeneratorOptions;
|
||||||
usernameGenerationOptions?: UsernameGeneratorOptions;
|
usernameGenerationOptions?: UsernameGeneratorOptions;
|
||||||
@ -210,7 +209,6 @@ export class AccountSettings {
|
|||||||
pinKeyEncryptedUserKey?: EncryptedString;
|
pinKeyEncryptedUserKey?: EncryptedString;
|
||||||
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
|
pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
|
||||||
protectedPin?: string;
|
protectedPin?: string;
|
||||||
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
|
|
||||||
vaultTimeout?: number;
|
vaultTimeout?: number;
|
||||||
vaultTimeoutAction?: string = "lock";
|
vaultTimeoutAction?: string = "lock";
|
||||||
serverConfig?: ServerConfigData;
|
serverConfig?: ServerConfigData;
|
||||||
@ -236,10 +234,6 @@ export class AccountSettings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AccountSettingsSettings = {
|
|
||||||
equivalentDomains?: string[][];
|
|
||||||
};
|
|
||||||
|
|
||||||
export class AccountTokens {
|
export class AccountTokens {
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
refreshToken?: string;
|
refreshToken?: string;
|
||||||
|
@ -25,6 +25,5 @@ export class GlobalState {
|
|||||||
enableBrowserIntegration?: boolean;
|
enableBrowserIntegration?: boolean;
|
||||||
enableBrowserIntegrationFingerprint?: boolean;
|
enableBrowserIntegrationFingerprint?: boolean;
|
||||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||||
neverDomains?: { [id: string]: unknown };
|
|
||||||
deepLinkRedirectUrl?: string;
|
deepLinkRedirectUrl?: string;
|
||||||
}
|
}
|
||||||
|
101
libs/common/src/platform/services/app-id.service.spec.ts
Normal file
101
libs/common/src/platform/services/app-id.service.spec.ts
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { FakeGlobalStateProvider } from "../../../spec";
|
||||||
|
import { Utils } from "../misc/utils";
|
||||||
|
|
||||||
|
import { ANONYMOUS_APP_ID_KEY, APP_ID_KEY, AppIdService } from "./app-id.service";
|
||||||
|
|
||||||
|
describe("AppIdService", () => {
|
||||||
|
const globalStateProvider = new FakeGlobalStateProvider();
|
||||||
|
const appIdState = globalStateProvider.getFake(APP_ID_KEY);
|
||||||
|
const anonymousAppIdState = globalStateProvider.getFake(ANONYMOUS_APP_ID_KEY);
|
||||||
|
let sut: AppIdService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sut = new AppIdService(globalStateProvider);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAppId", () => {
|
||||||
|
it("returns the existing appId when it exists", async () => {
|
||||||
|
appIdState.stateSubject.next("existingAppId");
|
||||||
|
|
||||||
|
const appId = await sut.getAppId();
|
||||||
|
|
||||||
|
expect(appId).toBe("existingAppId");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"uses the util function to create a new id when it AppId does not exist",
|
||||||
|
async (value) => {
|
||||||
|
appIdState.stateSubject.next(value);
|
||||||
|
const spy = jest.spyOn(Utils, "newGuid");
|
||||||
|
|
||||||
|
await sut.getAppId();
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
|
||||||
|
appIdState.stateSubject.next(value);
|
||||||
|
|
||||||
|
const appId = await sut.getAppId();
|
||||||
|
|
||||||
|
expect(appId).toMatch(Utils.guidRegex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"stores the new guid when it an existing one is not found",
|
||||||
|
async (value) => {
|
||||||
|
appIdState.stateSubject.next(value);
|
||||||
|
|
||||||
|
const appId = await sut.getAppId();
|
||||||
|
|
||||||
|
expect(appIdState.nextMock).toHaveBeenCalledWith(appId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getAnonymousAppId", () => {
|
||||||
|
it("returns the existing appId when it exists", async () => {
|
||||||
|
anonymousAppIdState.stateSubject.next("existingAppId");
|
||||||
|
|
||||||
|
const appId = await sut.getAnonymousAppId();
|
||||||
|
|
||||||
|
expect(appId).toBe("existingAppId");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"uses the util function to create a new id when it AppId does not exist",
|
||||||
|
async (value) => {
|
||||||
|
anonymousAppIdState.stateSubject.next(value);
|
||||||
|
const spy = jest.spyOn(Utils, "newGuid");
|
||||||
|
|
||||||
|
await sut.getAnonymousAppId();
|
||||||
|
|
||||||
|
expect(spy).toHaveBeenCalledTimes(1);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each([null, undefined])("returns a new appId when it does not exist", async (value) => {
|
||||||
|
anonymousAppIdState.stateSubject.next(value);
|
||||||
|
|
||||||
|
const appId = await sut.getAnonymousAppId();
|
||||||
|
|
||||||
|
expect(appId).toMatch(Utils.guidRegex);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([null, undefined])(
|
||||||
|
"stores the new guid when it an existing one is not found",
|
||||||
|
async (value) => {
|
||||||
|
anonymousAppIdState.stateSubject.next(value);
|
||||||
|
|
||||||
|
const appId = await sut.getAnonymousAppId();
|
||||||
|
|
||||||
|
expect(anonymousAppIdState.nextMock).toHaveBeenCalledWith(appId);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
@ -1,31 +1,46 @@
|
|||||||
|
import { Observable, filter, firstValueFrom, tap } from "rxjs";
|
||||||
|
|
||||||
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service";
|
import { AppIdService as AppIdServiceAbstraction } from "../abstractions/app-id.service";
|
||||||
import { AbstractStorageService } from "../abstractions/storage.service";
|
|
||||||
import { HtmlStorageLocation } from "../enums";
|
|
||||||
import { Utils } from "../misc/utils";
|
import { Utils } from "../misc/utils";
|
||||||
|
import { APPLICATION_ID_DISK, GlobalStateProvider, KeyDefinition } from "../state";
|
||||||
|
|
||||||
|
export const APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "appId", {
|
||||||
|
deserializer: (value: string) => value,
|
||||||
|
});
|
||||||
|
export const ANONYMOUS_APP_ID_KEY = new KeyDefinition(APPLICATION_ID_DISK, "anonymousAppId", {
|
||||||
|
deserializer: (value: string) => value,
|
||||||
|
});
|
||||||
|
|
||||||
export class AppIdService implements AppIdServiceAbstraction {
|
export class AppIdService implements AppIdServiceAbstraction {
|
||||||
constructor(private storageService: AbstractStorageService) {}
|
appId$: Observable<string>;
|
||||||
|
anonymousAppId$: Observable<string>;
|
||||||
|
|
||||||
getAppId(): Promise<string> {
|
constructor(globalStateProvider: GlobalStateProvider) {
|
||||||
return this.makeAndGetAppId("appId");
|
const appIdState = globalStateProvider.get(APP_ID_KEY);
|
||||||
|
const anonymousAppIdState = globalStateProvider.get(ANONYMOUS_APP_ID_KEY);
|
||||||
|
this.appId$ = appIdState.state$.pipe(
|
||||||
|
tap(async (appId) => {
|
||||||
|
if (!appId) {
|
||||||
|
await appIdState.update(() => Utils.newGuid());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
filter((appId) => !!appId),
|
||||||
|
);
|
||||||
|
this.anonymousAppId$ = anonymousAppIdState.state$.pipe(
|
||||||
|
tap(async (appId) => {
|
||||||
|
if (!appId) {
|
||||||
|
await anonymousAppIdState.update(() => Utils.newGuid());
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
filter((appId) => !!appId),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAnonymousAppId(): Promise<string> {
|
async getAppId(): Promise<string> {
|
||||||
return this.makeAndGetAppId("anonymousAppId");
|
return await firstValueFrom(this.appId$);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async makeAndGetAppId(key: string) {
|
async getAnonymousAppId(): Promise<string> {
|
||||||
const existingId = await this.storageService.get<string>(key, {
|
return await firstValueFrom(this.anonymousAppId$);
|
||||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
|
||||||
});
|
|
||||||
if (existingId != null) {
|
|
||||||
return existingId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const guid = Utils.newGuid();
|
|
||||||
await this.storageService.save(key, guid, {
|
|
||||||
htmlStorageLocation: HtmlStorageLocation.Local,
|
|
||||||
});
|
|
||||||
return guid;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,6 @@ import { SendData } from "../../tools/send/models/data/send.data";
|
|||||||
import { SendView } from "../../tools/send/models/view/send.view";
|
import { SendView } from "../../tools/send/models/view/send.view";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { DeviceKey, MasterKey } from "../../types/key";
|
import { DeviceKey, MasterKey } from "../../types/key";
|
||||||
import { UriMatchType } from "../../vault/enums";
|
|
||||||
import { CipherData } from "../../vault/models/data/cipher.data";
|
import { CipherData } from "../../vault/models/data/cipher.data";
|
||||||
import { LocalData } from "../../vault/models/data/local.data";
|
import { LocalData } from "../../vault/models/data/local.data";
|
||||||
import { CipherView } from "../../vault/models/view/cipher.view";
|
import { CipherView } from "../../vault/models/view/cipher.view";
|
||||||
@ -41,7 +40,6 @@ import {
|
|||||||
AccountData,
|
AccountData,
|
||||||
AccountDecryptionOptions,
|
AccountDecryptionOptions,
|
||||||
AccountSettings,
|
AccountSettings,
|
||||||
AccountSettingsSettings,
|
|
||||||
} from "../models/domain/account";
|
} from "../models/domain/account";
|
||||||
import { EncString } from "../models/domain/enc-string";
|
import { EncString } from "../models/domain/enc-string";
|
||||||
import { GlobalState } from "../models/domain/global-state";
|
import { GlobalState } from "../models/domain/global-state";
|
||||||
@ -786,23 +784,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDefaultUriMatch(options?: StorageOptions): Promise<UriMatchType> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.settings?.defaultUriMatch;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.defaultUriMatch = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getDisableFavicon(options?: StorageOptions): Promise<boolean> {
|
async getDisableFavicon(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(
|
(
|
||||||
@ -1304,23 +1285,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.settings?.equivalentDomains;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setEquivalentDomains(value: string, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
account.settings.equivalentDomains = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@withPrototypeForArrayMembers(EventData)
|
@withPrototypeForArrayMembers(EventData)
|
||||||
async getEventCollection(options?: StorageOptions): Promise<EventData[]> {
|
async getEventCollection(options?: StorageOptions): Promise<EventData[]> {
|
||||||
return (
|
return (
|
||||||
@ -1580,23 +1544,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: unknown }> {
|
|
||||||
return (
|
|
||||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
|
||||||
)?.neverDomains;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setNeverDomains(value: { [id: string]: unknown }, options?: StorageOptions): Promise<void> {
|
|
||||||
const globals = await this.getGlobals(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
globals.neverDomains = value;
|
|
||||||
await this.saveGlobals(
|
|
||||||
globals,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
|
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
|
||||||
return (
|
return (
|
||||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||||
@ -1778,23 +1725,6 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSettings(options?: StorageOptions): Promise<AccountSettingsSettings> {
|
|
||||||
return (
|
|
||||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
|
|
||||||
)?.settings?.settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setSettings(value: AccountSettingsSettings, options?: StorageOptions): Promise<void> {
|
|
||||||
const account = await this.getAccount(
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
|
|
||||||
);
|
|
||||||
account.settings.settings = value;
|
|
||||||
await this.saveAccount(
|
|
||||||
account,
|
|
||||||
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getTheme(options?: StorageOptions): Promise<ThemeType> {
|
async getTheme(options?: StorageOptions): Promise<ThemeType> {
|
||||||
return (
|
return (
|
||||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
@ -15,6 +15,7 @@ export interface GlobalState<T> {
|
|||||||
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
||||||
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
||||||
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
|
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
|
||||||
|
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
|
||||||
*/
|
*/
|
||||||
update: <TCombine>(
|
update: <TCombine>(
|
||||||
configureState: (state: T, dependency: TCombine) => T,
|
configureState: (state: T, dependency: TCombine) => T,
|
||||||
|
@ -27,6 +27,7 @@ export const PROVIDERS_DISK = new StateDefinition("providers", "disk");
|
|||||||
|
|
||||||
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
export const ACCOUNT_MEMORY = new StateDefinition("account", "memory");
|
||||||
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
export const SSO_DISK = new StateDefinition("ssoLogin", "disk");
|
||||||
|
export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory");
|
||||||
|
|
||||||
// Autofill
|
// Autofill
|
||||||
|
|
||||||
@ -38,6 +39,8 @@ export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
|
|||||||
|
|
||||||
// Billing
|
// Billing
|
||||||
|
|
||||||
|
export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk");
|
||||||
|
|
||||||
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
|
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
|
||||||
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
|
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
|
||||||
web: "disk-local",
|
web: "disk-local",
|
||||||
@ -52,6 +55,9 @@ export const NEW_WEB_LAYOUT_BANNER_DISK = new StateDefinition("newWebLayoutBanne
|
|||||||
|
|
||||||
// Platform
|
// Platform
|
||||||
|
|
||||||
|
export const APPLICATION_ID_DISK = new StateDefinition("applicationId", "disk", {
|
||||||
|
web: "disk-local",
|
||||||
|
});
|
||||||
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
|
export const BIOMETRIC_SETTINGS_DISK = new StateDefinition("biometricSettings", "disk");
|
||||||
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk");
|
||||||
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
export const CRYPTO_DISK = new StateDefinition("crypto", "disk");
|
||||||
|
@ -32,7 +32,8 @@ export interface ActiveUserState<T> extends UserState<T> {
|
|||||||
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
||||||
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
||||||
|
|
||||||
* @returns The new state
|
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
|
||||||
|
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
|
||||||
*/
|
*/
|
||||||
readonly update: <TCombine>(
|
readonly update: <TCombine>(
|
||||||
configureState: (state: T, dependencies: TCombine) => T,
|
configureState: (state: T, dependencies: TCombine) => T,
|
||||||
@ -50,7 +51,8 @@ export interface SingleUserState<T> extends UserState<T> {
|
|||||||
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
* @param options.combineLatestWith An observable that you want to combine with the current state for callbacks. Defaults to null
|
||||||
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
* @param options.msTimeout A timeout for how long you are willing to wait for a `combineLatestWith` option to complete. Defaults to 1000ms. Only applies if `combineLatestWith` is set.
|
||||||
|
|
||||||
* @returns The new state
|
* @returns A promise that must be awaited before your next action to ensure the update has been written to state.
|
||||||
|
* Resolves to the new state. If `shouldUpdate` returns false, the promise will resolve to the current state.
|
||||||
*/
|
*/
|
||||||
readonly update: <TCombine>(
|
readonly update: <TCombine>(
|
||||||
configureState: (state: T, dependencies: TCombine) => T,
|
configureState: (state: T, dependencies: TCombine) => T,
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user