mirror of
https://github.com/bitwarden/browser
synced 2025-12-13 14:53:33 +00:00
Login Flows (#4411)
* [SG-171] Login with a device request: Desktop (#3999) * Move LoginWithDeviceComponent to libs * Create login module * Remove login component from previous location * Move startPasswordlessLogin method to base class * Register route for login with device component * Add new localizations * Add Login with Device page styles * Add desktop login with device component * Spacing fix * Add content box around page * Update wording of helper text * Make resend timeout a class variable * SG-173 - Login device approval desktop (#4232) * SG-173 Implemented UI and login for login approval request * SG-173 - Show login approval after login * SG-173 Fetch login requests if the setting is true * SG-173 Add subheading to new setting * SG-173 Handle modal dismiss denying login request * SG-173 Fix pr comments * SG-173 Implemented desktop alerts * SG-173 Replicated behaviour of openViewRef * SG-173 Fixed previous commit * SG-173 PR fix * SG-173 Fix PR comment * SG-173 Added missing service injection * SG-173 Added logo to notifications * SG-173 Fix PR comments * [SG-910] Override self hosted check for desktop (#4405) * Override base component self hosted check * Add selfhost check to environment service * [SG-170] Login with Device Request - Browser (#4198) * work: ui stuff * fix: use parent * fix: words * [SG-987] [SG-988] [SG-989] Fix passwordless login request (#4573) * SG-987 Fix notification text and button options * SG-988 Fix approval and decline confirmation toasts * SG-989 Fix methods called * SG-988 Undo previous commit * [SG-1034] [Defect] - Vault is empty upon login confirmation (#4646) * fix: sync after login * undo: whoops --------- Co-authored-by: Carlos Gonçalves <cgoncalves@bitwarden.com> Co-authored-by: Brandon Maharaj <bmaharaj@bitwarden.com> Co-authored-by: Todd Martin <106564991+trmartin4@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dev_flags": {},
|
"dev_flags": {},
|
||||||
"flags": {}
|
"flags": {
|
||||||
|
"showPasswordless": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,5 +5,7 @@
|
|||||||
"base": "https://localhost:8080"
|
"base": "https://localhost:8080"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flags": {}
|
"flags": {
|
||||||
|
"showPasswordless": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2050,6 +2050,30 @@
|
|||||||
"rememberEmail": {
|
"rememberEmail": {
|
||||||
"message": "Remember email"
|
"message": "Remember email"
|
||||||
},
|
},
|
||||||
|
"loginWithDevice": {
|
||||||
|
"message": "Log in with device"
|
||||||
|
},
|
||||||
|
"loginWithDeviceEnabledInfo": {
|
||||||
|
"message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?"
|
||||||
|
},
|
||||||
|
"fingerprintPhraseHeader": {
|
||||||
|
"message": "Fingerprint phrase"
|
||||||
|
},
|
||||||
|
"fingerprintMatchInfo": {
|
||||||
|
"message": "Please make sure your vault is unlocked and the Fingerprint phrase matches on the other device."
|
||||||
|
},
|
||||||
|
"resendNotification": {
|
||||||
|
"message": "Resend notification"
|
||||||
|
},
|
||||||
|
"viewAllLoginOptions": {
|
||||||
|
"message": "View all log in options"
|
||||||
|
},
|
||||||
|
"notificationSentDevice": {
|
||||||
|
"message": "A notification has been sent to your device."
|
||||||
|
},
|
||||||
|
"logInInitiated": {
|
||||||
|
"message": "Log in initiated"
|
||||||
|
},
|
||||||
"exposedMasterPassword": {
|
"exposedMasterPassword": {
|
||||||
"message": "Exposed Master Password"
|
"message": "Exposed Master Password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -374,7 +374,8 @@ export default class MainBackground {
|
|||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.twoFactorService,
|
this.twoFactorService,
|
||||||
this.i18nService
|
this.i18nService,
|
||||||
|
this.encryptService
|
||||||
);
|
);
|
||||||
|
|
||||||
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService(
|
||||||
@@ -460,7 +461,8 @@ export default class MainBackground {
|
|||||||
logoutCallback,
|
logoutCallback,
|
||||||
this.logService,
|
this.logService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.authService
|
this.authService,
|
||||||
|
this.messagingService
|
||||||
);
|
);
|
||||||
this.popupUtilsService = new PopupUtilsService(isPrivateMode);
|
this.popupUtilsService = new PopupUtilsService(isPrivateMode);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { AuthService } from "@bitwarden/common/services/auth.service";
|
|||||||
import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory";
|
import { apiServiceFactory, ApiServiceInitOptions } from "./api-service.factory";
|
||||||
import { appIdServiceFactory } from "./app-id-service.factory";
|
import { appIdServiceFactory } from "./app-id-service.factory";
|
||||||
import { cryptoServiceFactory, CryptoServiceInitOptions } from "./crypto-service.factory";
|
import { cryptoServiceFactory, CryptoServiceInitOptions } from "./crypto-service.factory";
|
||||||
|
import { EncryptServiceInitOptions, encryptServiceFactory } from "./encrypt-service.factory";
|
||||||
import {
|
import {
|
||||||
environmentServiceFactory,
|
environmentServiceFactory,
|
||||||
EnvironmentServiceInitOptions,
|
EnvironmentServiceInitOptions,
|
||||||
@@ -37,7 +38,8 @@ export type AuthServiceInitOptions = AuthServiceFactoyOptions &
|
|||||||
EnvironmentServiceInitOptions &
|
EnvironmentServiceInitOptions &
|
||||||
StateServiceInitOptions &
|
StateServiceInitOptions &
|
||||||
TwoFactorServiceInitOptions &
|
TwoFactorServiceInitOptions &
|
||||||
I18nServiceInitOptions;
|
I18nServiceInitOptions &
|
||||||
|
EncryptServiceInitOptions;
|
||||||
|
|
||||||
export function authServiceFactory(
|
export function authServiceFactory(
|
||||||
cache: { authService?: AbstractAuthService } & CachedServices,
|
cache: { authService?: AbstractAuthService } & CachedServices,
|
||||||
@@ -60,7 +62,8 @@ export function authServiceFactory(
|
|||||||
await environmentServiceFactory(cache, opts),
|
await environmentServiceFactory(cache, opts),
|
||||||
await stateServiceFactory(cache, opts),
|
await stateServiceFactory(cache, opts),
|
||||||
await twoFactorServiceFactory(cache, opts),
|
await twoFactorServiceFactory(cache, opts),
|
||||||
await i18nServiceFactory(cache, opts)
|
await i18nServiceFactory(cache, opts),
|
||||||
|
await encryptServiceFactory(cache, opts)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<div class="login-with-device">
|
||||||
|
<header>
|
||||||
|
<h1 class="login-center">
|
||||||
|
<span class="title">{{ "logIn" | i18n }}</span>
|
||||||
|
</h1>
|
||||||
|
</header>
|
||||||
|
<div class="content login-page">
|
||||||
|
<div>
|
||||||
|
<p class="lead">{{ "logInInitiated" | i18n }}</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>{{ "notificationSentDevice" | i18n }}</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{{ "fingerprintMatchInfo" | i18n }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<b class="fingerprint-phrase-header">{{ "fingerprintPhraseHeader" | i18n }}</b>
|
||||||
|
<p class="fingerprint-text">
|
||||||
|
<code>{{ passwordlessRequest?.fingerprintPhrase }}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="resend-notification" *ngIf="showResendNotification">
|
||||||
|
<a (click)="startPasswordlessLogin()">{{ "resendNotification" | i18n }}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
{{ "loginWithDeviceEnabledInfo" | i18n }}
|
||||||
|
<a routerLink="/login">{{ "viewAllLoginOptions" | i18n }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/components/login-with-device.component";
|
||||||
|
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
|
import { LoginService } from "@bitwarden/common/abstractions/login.service";
|
||||||
|
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
|
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-login-with-device",
|
||||||
|
templateUrl: "login-with-device.component.html",
|
||||||
|
})
|
||||||
|
export class LoginWithDeviceComponent
|
||||||
|
extends BaseLoginWithDeviceComponent
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
router: Router,
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
appIdService: AppIdService,
|
||||||
|
passwordGenerationService: PasswordGenerationService,
|
||||||
|
apiService: ApiService,
|
||||||
|
authService: AuthService,
|
||||||
|
logService: LogService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
anonymousHubService: AnonymousHubService,
|
||||||
|
validationService: ValidationService,
|
||||||
|
stateService: StateService,
|
||||||
|
loginService: LoginService,
|
||||||
|
syncService: SyncService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
router,
|
||||||
|
cryptoService,
|
||||||
|
cryptoFunctionService,
|
||||||
|
appIdService,
|
||||||
|
passwordGenerationService,
|
||||||
|
apiService,
|
||||||
|
authService,
|
||||||
|
logService,
|
||||||
|
environmentService,
|
||||||
|
i18nService,
|
||||||
|
platformUtilsService,
|
||||||
|
anonymousHubService,
|
||||||
|
validationService,
|
||||||
|
stateService,
|
||||||
|
loginService
|
||||||
|
);
|
||||||
|
super.onSuccessfulLogin = async () => {
|
||||||
|
await syncService.fullSync(true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,13 @@
|
|||||||
<div class="box-content">
|
<div class="box-content">
|
||||||
<div class="box-content-row box-content-row-flex" appBoxRow>
|
<div class="box-content-row box-content-row-flex" appBoxRow>
|
||||||
<div class="row-main">
|
<div class="row-main">
|
||||||
|
<input id="email" type="text" formControlName="email" [hidden]="true" />
|
||||||
|
<input
|
||||||
|
id="rememberEmail"
|
||||||
|
type="checkbox"
|
||||||
|
formControlName="rememberEmail"
|
||||||
|
[hidden]="true"
|
||||||
|
/>
|
||||||
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
|
||||||
<input
|
<input
|
||||||
id="masterPassword"
|
id="masterPassword"
|
||||||
@@ -54,6 +61,11 @@
|
|||||||
>
|
>
|
||||||
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-lg bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<div class="tw-mb-3" *ngIf="showLoginWithDevice && showPasswordless">
|
||||||
|
<button type="button" class="btn block" (click)="startPasswordlessLogin()">
|
||||||
|
<span> <i class="bwi bwi-mobile"></i> {{ "loginWithDevice" | i18n }} </span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button type="button" (click)="launchSsoBrowser()" class="btn block">
|
<button type="button" (click)="launchSsoBrowser()" class="btn block">
|
||||||
<i class="bwi bwi-provider" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
|
<i class="bwi bwi-provider" aria-hidden="true"></i> {{ "enterpriseSingleSignOn" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -18,11 +18,14 @@ import { StateService } from "@bitwarden/common/abstractions/state.service";
|
|||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
|
import { flagEnabled } from "../../flags";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-login",
|
selector: "app-login",
|
||||||
templateUrl: "login.component.html",
|
templateUrl: "login.component.html",
|
||||||
})
|
})
|
||||||
export class LoginComponent extends BaseLoginComponent {
|
export class LoginComponent extends BaseLoginComponent {
|
||||||
|
showPasswordless = false;
|
||||||
constructor(
|
constructor(
|
||||||
apiService: ApiService,
|
apiService: ApiService,
|
||||||
appIdService: AppIdService,
|
appIdService: AppIdService,
|
||||||
@@ -64,6 +67,13 @@ export class LoginComponent extends BaseLoginComponent {
|
|||||||
await syncService.fullSync(true);
|
await syncService.fullSync(true);
|
||||||
};
|
};
|
||||||
super.successRoute = "/tabs/vault";
|
super.successRoute = "/tabs/vault";
|
||||||
|
this.showPasswordless = flagEnabled("showPasswordless");
|
||||||
|
|
||||||
|
if (this.showPasswordless) {
|
||||||
|
this.formGroup.controls.email.setValue(this.loginService.getEmail());
|
||||||
|
this.formGroup.controls.rememberEmail.setValue(this.loginService.getRememberEmail());
|
||||||
|
this.validateEmail();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
settings() {
|
settings() {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export const routerTransition = trigger("routerTransition", [
|
|||||||
|
|
||||||
transition("login => home", outSlideDown),
|
transition("login => home", outSlideDown),
|
||||||
transition("login => hint", inSlideUp),
|
transition("login => hint", inSlideUp),
|
||||||
transition("login => tabs, login => 2fa", inSlideLeft),
|
transition("login => tabs, login => 2fa, login => login-with-device", inSlideLeft),
|
||||||
|
|
||||||
transition("hint => login, register => home, environment => home", outSlideDown),
|
transition("hint => login, register => home, environment => home", outSlideDown),
|
||||||
|
|
||||||
@@ -129,6 +129,9 @@ export const routerTransition = trigger("routerTransition", [
|
|||||||
transition("2fa-options => 2fa", outSlideDown),
|
transition("2fa-options => 2fa", outSlideDown),
|
||||||
transition("2fa => tabs", inSlideLeft),
|
transition("2fa => tabs", inSlideLeft),
|
||||||
|
|
||||||
|
transition("login-with-device => tabs, login-with-device => 2fa", inSlideLeft),
|
||||||
|
transition("login-with-device => login", outSlideRight),
|
||||||
|
|
||||||
transition(tabsToCiphers, inSlideLeft),
|
transition(tabsToCiphers, inSlideLeft),
|
||||||
transition(ciphersToTabs, outSlideRight),
|
transition(ciphersToTabs, outSlideRight),
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { EnvironmentComponent } from "./accounts/environment.component";
|
|||||||
import { HintComponent } from "./accounts/hint.component";
|
import { HintComponent } from "./accounts/hint.component";
|
||||||
import { HomeComponent } from "./accounts/home.component";
|
import { HomeComponent } from "./accounts/home.component";
|
||||||
import { LockComponent } from "./accounts/lock.component";
|
import { LockComponent } from "./accounts/lock.component";
|
||||||
|
import { LoginWithDeviceComponent } from "./accounts/login-with-device.component";
|
||||||
import { LoginComponent } from "./accounts/login.component";
|
import { LoginComponent } from "./accounts/login.component";
|
||||||
import { RegisterComponent } from "./accounts/register.component";
|
import { RegisterComponent } from "./accounts/register.component";
|
||||||
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
||||||
@@ -67,6 +68,12 @@ const routes: Routes = [
|
|||||||
canActivate: [UnauthGuard],
|
canActivate: [UnauthGuard],
|
||||||
data: { state: "login" },
|
data: { state: "login" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "login-with-device",
|
||||||
|
component: LoginWithDeviceComponent,
|
||||||
|
canActivate: [UnauthGuard],
|
||||||
|
data: { state: "login-with-device" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "lock",
|
path: "lock",
|
||||||
component: LockComponent,
|
component: LockComponent,
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { EnvironmentComponent } from "./accounts/environment.component";
|
|||||||
import { HintComponent } from "./accounts/hint.component";
|
import { HintComponent } from "./accounts/hint.component";
|
||||||
import { HomeComponent } from "./accounts/home.component";
|
import { HomeComponent } from "./accounts/home.component";
|
||||||
import { LockComponent } from "./accounts/lock.component";
|
import { LockComponent } from "./accounts/lock.component";
|
||||||
|
import { LoginWithDeviceComponent } from "./accounts/login-with-device.component";
|
||||||
import { LoginComponent } from "./accounts/login.component";
|
import { LoginComponent } from "./accounts/login.component";
|
||||||
import { RegisterComponent } from "./accounts/register.component";
|
import { RegisterComponent } from "./accounts/register.component";
|
||||||
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
||||||
@@ -117,6 +118,7 @@ import { TabsComponent } from "./tabs.component";
|
|||||||
HomeComponent,
|
HomeComponent,
|
||||||
LockComponent,
|
LockComponent,
|
||||||
LoginComponent,
|
LoginComponent,
|
||||||
|
LoginWithDeviceComponent,
|
||||||
OptionsComponent,
|
OptionsComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
PasswordGeneratorHistoryComponent,
|
PasswordGeneratorHistoryComponent,
|
||||||
|
|||||||
@@ -622,3 +622,35 @@ main {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-with-device {
|
||||||
|
.fingerprint-phrase-header {
|
||||||
|
padding-top: 1rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include themify($themes) {
|
||||||
|
.fingerprint-text {
|
||||||
|
color: themed("codeColor");
|
||||||
|
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||||
|
monospace;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.resend-notification {
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding-top: 1rem;
|
||||||
|
|
||||||
|
a {
|
||||||
|
padding-top: 1rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ $themes: (
|
|||||||
// light has no hover so use same color
|
// light has no hover so use same color
|
||||||
webkitCalendarPickerHoverFilter: invert(46%) sepia(69%) saturate(6397%) hue-rotate(211deg)
|
webkitCalendarPickerHoverFilter: invert(46%) sepia(69%) saturate(6397%) hue-rotate(211deg)
|
||||||
brightness(85%) contrast(103%),
|
brightness(85%) contrast(103%),
|
||||||
|
codeColor: #e83e8c,
|
||||||
),
|
),
|
||||||
dark: (
|
dark: (
|
||||||
textColor: #ffffff,
|
textColor: #ffffff,
|
||||||
@@ -183,6 +184,7 @@ $themes: (
|
|||||||
hue-rotate(184deg) brightness(87%) contrast(93%),
|
hue-rotate(184deg) brightness(87%) contrast(93%),
|
||||||
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(100%) sepia(0%)
|
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(100%) sepia(0%)
|
||||||
saturate(0%) hue-rotate(93deg) brightness(103%) contrast(103%),
|
saturate(0%) hue-rotate(93deg) brightness(103%) contrast(103%),
|
||||||
|
codeColor: #e83e8c,
|
||||||
),
|
),
|
||||||
nord: (
|
nord: (
|
||||||
textColor: $nord5,
|
textColor: $nord5,
|
||||||
@@ -244,6 +246,7 @@ $themes: (
|
|||||||
// has no hover so use same color
|
// has no hover so use same color
|
||||||
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(5%)
|
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(5%)
|
||||||
saturate(454%) hue-rotate(185deg) brightness(93%) contrast(96%),
|
saturate(454%) hue-rotate(185deg) brightness(93%) contrast(96%),
|
||||||
|
codeColor: #e83e8c,
|
||||||
),
|
),
|
||||||
solarizedDark: (
|
solarizedDark: (
|
||||||
textColor: $solarizedDarkBase2,
|
textColor: $solarizedDarkBase2,
|
||||||
@@ -304,6 +307,7 @@ $themes: (
|
|||||||
hue-rotate(138deg) brightness(92%) contrast(90%),
|
hue-rotate(138deg) brightness(92%) contrast(90%),
|
||||||
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(10%)
|
webkitCalendarPickerHoverFilter: brightness(0) saturate(100%) invert(94%) sepia(10%)
|
||||||
saturate(462%) hue-rotate(345deg) brightness(103%) contrast(87%),
|
saturate(462%) hue-rotate(345deg) brightness(103%) contrast(87%),
|
||||||
|
codeColor: #e83e8c,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -286,7 +286,8 @@ export class Main {
|
|||||||
this.environmentService,
|
this.environmentService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
this.twoFactorService,
|
this.twoFactorService,
|
||||||
this.i18nService
|
this.i18nService,
|
||||||
|
this.encryptService
|
||||||
);
|
);
|
||||||
|
|
||||||
const lockedCallback = async () =>
|
const lockedCallback = async () =>
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="loginApprovalTitle">
|
||||||
|
<div class="modal-dialog modal-md" role="document">
|
||||||
|
<div id="login-approval-page" class="modal-content">
|
||||||
|
<div class="section-title">
|
||||||
|
<p style="text-transform: uppercase">{{ "areYouTryingtoLogin" | i18n }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<div class="section">
|
||||||
|
<h4>{{ "logInAttemptBy" | i18n: email }}</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h4 class="label">{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||||
|
<code>{{ authRequestResponse?.requestFingerprint }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h4 class="label">{{ "deviceType" | i18n }}</h4>
|
||||||
|
<p>{{ authRequestResponse?.requestDeviceType }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h4 class="label">{{ "ipAddress" | i18n }}</h4>
|
||||||
|
<p>{{ authRequestResponse?.requestIpAddress }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h4 class="label">{{ "time" | i18n }}</h4>
|
||||||
|
<p>{{ requestTimeText }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="primary" (click)="approveLogin(true, true)">
|
||||||
|
{{ "confirmLogIn" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" (click)="approveLogin(false, true)">
|
||||||
|
{{ "denyLogIn" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
170
apps/desktop/src/app/accounts/login/login-approval.component.ts
Normal file
170
apps/desktop/src/app/accounts/login/login-approval.component.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { Component, OnInit, OnDestroy } from "@angular/core";
|
||||||
|
import { ipcRenderer } from "electron";
|
||||||
|
import { Subject } from "rxjs";
|
||||||
|
|
||||||
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
|
import { ModalConfig } from "@bitwarden/angular/services/modal.service";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
|
import { AuthRequestResponse } from "@bitwarden/common/models/response/auth-request.response";
|
||||||
|
|
||||||
|
const RequestTimeOut = 60000 * 15; //15 Minutes
|
||||||
|
const RequestTimeUpdate = 60000 * 5; //5 Minutes
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "login-approval",
|
||||||
|
templateUrl: "login-approval.component.html",
|
||||||
|
})
|
||||||
|
export class LoginApprovalComponent implements OnInit, OnDestroy {
|
||||||
|
notificationId: string;
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
email: string;
|
||||||
|
authRequestResponse: AuthRequestResponse;
|
||||||
|
interval: NodeJS.Timer;
|
||||||
|
requestTimeText: string;
|
||||||
|
dismissModal: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected stateService: StateService,
|
||||||
|
protected platformUtilsService: PlatformUtilsService,
|
||||||
|
protected i18nService: I18nService,
|
||||||
|
protected apiService: ApiService,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected appIdService: AppIdService,
|
||||||
|
private modalRef: ModalRef,
|
||||||
|
config: ModalConfig
|
||||||
|
) {
|
||||||
|
this.notificationId = config.data.notificationId;
|
||||||
|
|
||||||
|
this.dismissModal = true;
|
||||||
|
this.modalRef.onClosed
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
.subscribe(() => {
|
||||||
|
if (this.dismissModal) {
|
||||||
|
this.approveLogin(false, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (this.notificationId != null) {
|
||||||
|
this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId);
|
||||||
|
|
||||||
|
this.email = await this.stateService.getEmail();
|
||||||
|
this.updateTimeText();
|
||||||
|
|
||||||
|
this.interval = setInterval(() => {
|
||||||
|
this.updateTimeText();
|
||||||
|
}, RequestTimeUpdate);
|
||||||
|
|
||||||
|
const isVisible = await ipcRenderer.invoke("windowVisible");
|
||||||
|
if (!isVisible) {
|
||||||
|
await ipcRenderer.invoke("loginRequest", {
|
||||||
|
alertTitle: this.i18nService.t("logInRequested"),
|
||||||
|
alertBody: this.i18nService.t("confirmLoginAtemptForMail", this.email),
|
||||||
|
buttonText: this.i18nService.t("close"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async approveLogin(approveLogin: boolean, approveDenyButtonClicked: boolean) {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
|
||||||
|
this.dismissModal = !approveDenyButtonClicked;
|
||||||
|
if (approveDenyButtonClicked) {
|
||||||
|
this.modalRef.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.authRequestResponse = await this.apiService.getAuthRequest(this.notificationId);
|
||||||
|
if (this.authRequestResponse.requestApproved || this.authRequestResponse.responseDate != null) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"info",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("thisRequestIsNoLongerValid")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const loginResponse = await this.authService.passwordlessLogin(
|
||||||
|
this.authRequestResponse.id,
|
||||||
|
this.authRequestResponse.publicKey,
|
||||||
|
approveLogin
|
||||||
|
);
|
||||||
|
this.showResultToast(loginResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
showResultToast(loginResponse: AuthRequestResponse) {
|
||||||
|
if (loginResponse.requestApproved) {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t(
|
||||||
|
"logInConfirmedForEmailOnDevice",
|
||||||
|
this.email,
|
||||||
|
loginResponse.requestDeviceType
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"info",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("youDeniedALogInAttemptFromAnotherDevice")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeText() {
|
||||||
|
const requestDate = new Date(this.authRequestResponse.creationDate);
|
||||||
|
const requestDateUTC = Date.UTC(
|
||||||
|
requestDate.getUTCFullYear(),
|
||||||
|
requestDate.getUTCMonth(),
|
||||||
|
requestDate.getDate(),
|
||||||
|
requestDate.getUTCHours(),
|
||||||
|
requestDate.getUTCMinutes(),
|
||||||
|
requestDate.getUTCSeconds(),
|
||||||
|
requestDate.getUTCMilliseconds()
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateNow = new Date(Date.now());
|
||||||
|
const dateNowUTC = Date.UTC(
|
||||||
|
dateNow.getUTCFullYear(),
|
||||||
|
dateNow.getUTCMonth(),
|
||||||
|
dateNow.getDate(),
|
||||||
|
dateNow.getUTCHours(),
|
||||||
|
dateNow.getUTCMinutes(),
|
||||||
|
dateNow.getUTCSeconds(),
|
||||||
|
dateNow.getUTCMilliseconds()
|
||||||
|
);
|
||||||
|
|
||||||
|
const diffInMinutes = dateNowUTC - requestDateUTC;
|
||||||
|
|
||||||
|
if (diffInMinutes <= RequestTimeUpdate) {
|
||||||
|
this.requestTimeText = this.i18nService.t("justNow");
|
||||||
|
} else if (diffInMinutes < RequestTimeOut) {
|
||||||
|
this.requestTimeText = this.i18nService.t(
|
||||||
|
"requestedXMinutesAgo",
|
||||||
|
(diffInMinutes / 60000).toFixed()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
clearInterval(this.interval);
|
||||||
|
this.modalRef.close();
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"info",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("loginRequestHasAlreadyExpired")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<div id="login-with-device-page">
|
||||||
|
<div class="login-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
appStopClick
|
||||||
|
(click)="settings()"
|
||||||
|
class="environment-urls-settings-icon"
|
||||||
|
attr.aria-label="{{ 'settings' | i18n }}"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
||||||
|
{{ "settings" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="content" class="content">
|
||||||
|
<img class="logo-image" alt="Bitwarden" />
|
||||||
|
<p class="lead text-center">{{ "logInInitiated" | i18n }}</p>
|
||||||
|
|
||||||
|
<div class="box last">
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="box-content-row" appBoxRow>
|
||||||
|
<div class="section">
|
||||||
|
<p class="section">{{ "notificationSentDevice" | i18n }}</p>
|
||||||
|
<p>
|
||||||
|
{{ "fingerprintMatchInfo" | i18n }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fingerprint section">
|
||||||
|
<h4>{{ "fingerprintPhraseHeader" | i18n }}</h4>
|
||||||
|
<code>{{ passwordlessRequest?.fingerprintPhrase }}</code>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section" *ngIf="showResendNotification">
|
||||||
|
<a [routerLink]="[]" disabled="true" (click)="startPasswordlessLogin()">{{
|
||||||
|
"resendNotification" | i18n
|
||||||
|
}}</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sub-options another-method">
|
||||||
|
<p class="no-margin description-text">
|
||||||
|
{{ "needAnotherOption" | i18n }}
|
||||||
|
<a type="button" class="text text-primary" (click)="goToLogin()">
|
||||||
|
{{ "viewAllLoginOptions" | i18n }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ng-template #environment></ng-template>
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
|
||||||
|
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/components/login-with-device.component";
|
||||||
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
|
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
|
import { LoginService } from "@bitwarden/common/abstractions/login.service";
|
||||||
|
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
|
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||||
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
|
import { EnvironmentComponent } from "../environment.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-login-with-device",
|
||||||
|
templateUrl: "login-with-device.component.html",
|
||||||
|
})
|
||||||
|
export class LoginWithDeviceComponent
|
||||||
|
extends BaseLoginWithDeviceComponent
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
|
@ViewChild("environment", { read: ViewContainerRef, static: true })
|
||||||
|
environmentModal: ViewContainerRef;
|
||||||
|
showingModal = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected router: Router,
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
|
appIdService: AppIdService,
|
||||||
|
passwordGenerationService: PasswordGenerationService,
|
||||||
|
apiService: ApiService,
|
||||||
|
authService: AuthService,
|
||||||
|
logService: LogService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
anonymousHubService: AnonymousHubService,
|
||||||
|
validationService: ValidationService,
|
||||||
|
private modalService: ModalService,
|
||||||
|
syncService: SyncService,
|
||||||
|
stateService: StateService,
|
||||||
|
loginService: LoginService
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
router,
|
||||||
|
cryptoService,
|
||||||
|
cryptoFunctionService,
|
||||||
|
appIdService,
|
||||||
|
passwordGenerationService,
|
||||||
|
apiService,
|
||||||
|
authService,
|
||||||
|
logService,
|
||||||
|
environmentService,
|
||||||
|
i18nService,
|
||||||
|
platformUtilsService,
|
||||||
|
anonymousHubService,
|
||||||
|
validationService,
|
||||||
|
stateService,
|
||||||
|
loginService
|
||||||
|
);
|
||||||
|
|
||||||
|
super.onSuccessfulLogin = () => {
|
||||||
|
return syncService.fullSync(true);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async settings() {
|
||||||
|
const [modal, childComponent] = await this.modalService.openViewRef(
|
||||||
|
EnvironmentComponent,
|
||||||
|
this.environmentModal
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
modal.onShown.subscribe(() => {
|
||||||
|
this.showingModal = true;
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
modal.onClosed.subscribe(() => {
|
||||||
|
this.showingModal = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
childComponent.onSaved.subscribe(() => {
|
||||||
|
modal.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
goToLogin() {
|
||||||
|
this.router.navigate(["/login"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -126,6 +126,12 @@
|
|||||||
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="buttons-row" *ngIf="showLoginWithDevice">
|
||||||
|
<button type="button" class="btn block" (click)="startPasswordlessLogin()">
|
||||||
|
<i class="bwi bwi-mobile" aria-hidden="true"></i>
|
||||||
|
{{ "logInWithAnotherDevice" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="buttons-row">
|
<div class="buttons-row">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -20,7 +20,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
|||||||
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
|
||||||
import { EnvironmentComponent } from "./environment.component";
|
import { EnvironmentComponent } from "../environment.component";
|
||||||
|
|
||||||
const BroadcasterSubscriptionId = "LoginComponent";
|
const BroadcasterSubscriptionId = "LoginComponent";
|
||||||
|
|
||||||
@@ -93,6 +93,7 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
|||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
await super.ngOnInit();
|
await super.ngOnInit();
|
||||||
|
await this.checkSelfHosted();
|
||||||
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
|
||||||
this.ngZone.run(() => {
|
this.ngZone.run(() => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
@@ -136,9 +137,10 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
|||||||
this.showingModal = false;
|
this.showingModal = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
||||||
childComponent.onSaved.subscribe(() => {
|
childComponent.onSaved.subscribe(async () => {
|
||||||
modal.close();
|
modal.close();
|
||||||
|
await this.checkSelfHosted();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,4 +177,10 @@ export class LoginComponent extends BaseLoginComponent implements OnDestroy {
|
|||||||
const email = this.loggedEmail;
|
const email = this.loggedEmail;
|
||||||
document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus();
|
document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async checkSelfHosted() {
|
||||||
|
this.selfHosted = this.environmentService.isSelfHosted();
|
||||||
|
|
||||||
|
await this.getLoginWithDevice(this.loggedEmail);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
14
apps/desktop/src/app/accounts/login/login.module.ts
Normal file
14
apps/desktop/src/app/accounts/login/login.module.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
import { RouterModule } from "@angular/router";
|
||||||
|
|
||||||
|
import { SharedModule } from "../../shared/shared.module";
|
||||||
|
|
||||||
|
import { LoginWithDeviceComponent } from "./login-with-device.component";
|
||||||
|
import { LoginComponent } from "./login.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, RouterModule],
|
||||||
|
declarations: [LoginComponent, LoginWithDeviceComponent],
|
||||||
|
exports: [LoginComponent, LoginWithDeviceComponent],
|
||||||
|
})
|
||||||
|
export class LoginModule {}
|
||||||
@@ -114,6 +114,21 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="checkbox">
|
||||||
|
<label for="approveLoginRequests">
|
||||||
|
<input
|
||||||
|
id="approveLoginRequests"
|
||||||
|
type="checkbox"
|
||||||
|
name="approveLoginRequests"
|
||||||
|
[(ngModel)]="approveLoginRequests"
|
||||||
|
(change)="updateApproveLoginRequests()"
|
||||||
|
/>
|
||||||
|
{{ "approveLoginRequests" | i18n }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small class="help-block">{{ "approveLoginRequestDesc" | i18n }}</small>
|
||||||
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
openAtLogin: boolean;
|
openAtLogin: boolean;
|
||||||
requireEnableTray = false;
|
requireEnableTray = false;
|
||||||
showDuckDuckGoIntegrationOption = false;
|
showDuckDuckGoIntegrationOption = false;
|
||||||
|
approveLoginRequests = false;
|
||||||
|
|
||||||
enableTrayText: string;
|
enableTrayText: string;
|
||||||
enableTrayDescText: string;
|
enableTrayDescText: string;
|
||||||
@@ -190,6 +191,7 @@ export class SettingsComponent implements OnInit {
|
|||||||
|
|
||||||
const pinSet = await this.vaultTimeoutSettingsService.isPinLockSet();
|
const pinSet = await this.vaultTimeoutSettingsService.isPinLockSet();
|
||||||
this.pin = pinSet[0] || pinSet[1];
|
this.pin = pinSet[0] || pinSet[1];
|
||||||
|
this.approveLoginRequests = await this.stateService.getApproveLoginRequests();
|
||||||
|
|
||||||
// Account preferences
|
// Account preferences
|
||||||
this.enableFavicons = !(await this.stateService.getDisableFavicon());
|
this.enableFavicons = !(await this.stateService.getDisableFavicon());
|
||||||
@@ -461,4 +463,8 @@ export class SettingsComponent implements OnInit {
|
|||||||
this.enableBrowserIntegrationFingerprint
|
this.enableBrowserIntegrationFingerprint
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async updateApproveLoginRequests() {
|
||||||
|
await this.stateService.setApproveLoginRequests(this.approveLoginRequests);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { VaultComponent } from "../vault/app/vault/vault.component";
|
|||||||
import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component";
|
import { AccessibilityCookieComponent } from "./accounts/accessibility-cookie.component";
|
||||||
import { HintComponent } from "./accounts/hint.component";
|
import { HintComponent } from "./accounts/hint.component";
|
||||||
import { LockComponent } from "./accounts/lock.component";
|
import { LockComponent } from "./accounts/lock.component";
|
||||||
import { LoginComponent } from "./accounts/login.component";
|
import { LoginWithDeviceComponent } from "./accounts/login/login-with-device.component";
|
||||||
|
import { LoginComponent } from "./accounts/login/login.component";
|
||||||
import { RegisterComponent } from "./accounts/register.component";
|
import { RegisterComponent } from "./accounts/register.component";
|
||||||
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
||||||
import { SetPasswordComponent } from "./accounts/set-password.component";
|
import { SetPasswordComponent } from "./accounts/set-password.component";
|
||||||
@@ -31,6 +32,10 @@ const routes: Routes = [
|
|||||||
component: LoginComponent,
|
component: LoginComponent,
|
||||||
canActivate: [LoginGuard],
|
canActivate: [LoginGuard],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "login-with-device",
|
||||||
|
component: LoginWithDeviceComponent,
|
||||||
|
},
|
||||||
{ path: "2fa", component: TwoFactorComponent },
|
{ path: "2fa", component: TwoFactorComponent },
|
||||||
{ path: "register", component: RegisterComponent },
|
{ path: "register", component: RegisterComponent },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { PremiumComponent } from "../vault/app/accounts/premium.component";
|
|||||||
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/app/vault/folder-add-edit.component";
|
||||||
|
|
||||||
import { DeleteAccountComponent } from "./accounts/delete-account.component";
|
import { DeleteAccountComponent } from "./accounts/delete-account.component";
|
||||||
|
import { LoginApprovalComponent } from "./accounts/login/login-approval.component";
|
||||||
import { SettingsComponent } from "./accounts/settings.component";
|
import { SettingsComponent } from "./accounts/settings.component";
|
||||||
import { ExportComponent } from "./vault/export.component";
|
import { ExportComponent } from "./vault/export.component";
|
||||||
import { GeneratorComponent } from "./vault/generator.component";
|
import { GeneratorComponent } from "./vault/generator.component";
|
||||||
@@ -70,6 +71,7 @@ const systemTimeoutOptions = {
|
|||||||
<ng-template #appFolderAddEdit></ng-template>
|
<ng-template #appFolderAddEdit></ng-template>
|
||||||
<ng-template #exportVault></ng-template>
|
<ng-template #exportVault></ng-template>
|
||||||
<ng-template #appGenerator></ng-template>
|
<ng-template #appGenerator></ng-template>
|
||||||
|
<ng-template #loginApproval></ng-template>
|
||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<div class="loading" *ngIf="loading">
|
<div class="loading" *ngIf="loading">
|
||||||
@@ -90,6 +92,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
folderAddEditModalRef: ViewContainerRef;
|
folderAddEditModalRef: ViewContainerRef;
|
||||||
@ViewChild("appGenerator", { read: ViewContainerRef, static: true })
|
@ViewChild("appGenerator", { read: ViewContainerRef, static: true })
|
||||||
generatorModalRef: ViewContainerRef;
|
generatorModalRef: ViewContainerRef;
|
||||||
|
@ViewChild("loginApproval", { read: ViewContainerRef, static: true })
|
||||||
|
loginApprovalModalRef: ViewContainerRef;
|
||||||
|
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
||||||
@@ -359,6 +363,11 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
case "systemIdle":
|
case "systemIdle":
|
||||||
await this.checkForSystemTimeout(systemTimeoutOptions.onIdle);
|
await this.checkForSystemTimeout(systemTimeoutOptions.onIdle);
|
||||||
break;
|
break;
|
||||||
|
case "openLoginApproval":
|
||||||
|
if (message.notificationId != null) {
|
||||||
|
await this.openLoginApproval(message.notificationId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -427,6 +436,19 @@ export class AppComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async openLoginApproval(notificationId: string) {
|
||||||
|
this.modalService.closeAll();
|
||||||
|
|
||||||
|
this.modal = await this.modalService.open(LoginApprovalComponent, {
|
||||||
|
data: { notificationId: notificationId },
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
||||||
|
this.modal.onClosed.subscribe(() => {
|
||||||
|
this.modal = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async updateAppMenu() {
|
private async updateAppMenu() {
|
||||||
let updateRequest: MenuUpdateRequest;
|
let updateRequest: MenuUpdateRequest;
|
||||||
const stateAccounts = await firstValueFrom(this.stateService.accounts$);
|
const stateAccounts = await firstValueFrom(this.stateService.accounts$);
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import { DeleteAccountComponent } from "./accounts/delete-account.component";
|
|||||||
import { EnvironmentComponent } from "./accounts/environment.component";
|
import { EnvironmentComponent } from "./accounts/environment.component";
|
||||||
import { HintComponent } from "./accounts/hint.component";
|
import { HintComponent } from "./accounts/hint.component";
|
||||||
import { LockComponent } from "./accounts/lock.component";
|
import { LockComponent } from "./accounts/lock.component";
|
||||||
import { LoginComponent } from "./accounts/login.component";
|
import { LoginApprovalComponent } from "./accounts/login/login-approval.component";
|
||||||
|
import { LoginModule } from "./accounts/login/login.module";
|
||||||
import { RegisterComponent } from "./accounts/register.component";
|
import { RegisterComponent } from "./accounts/register.component";
|
||||||
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
import { RemovePasswordComponent } from "./accounts/remove-password.component";
|
||||||
import { SetPasswordComponent } from "./accounts/set-password.component";
|
import { SetPasswordComponent } from "./accounts/set-password.component";
|
||||||
@@ -55,7 +56,7 @@ import { GeneratorComponent } from "./vault/generator.component";
|
|||||||
import { PasswordGeneratorHistoryComponent } from "./vault/password-generator-history.component";
|
import { PasswordGeneratorHistoryComponent } from "./vault/password-generator-history.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, AppRoutingModule, VaultFilterModule],
|
imports: [SharedModule, AppRoutingModule, VaultFilterModule, LoginModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
AccessibilityCookieComponent,
|
AccessibilityCookieComponent,
|
||||||
AccountSwitcherComponent,
|
AccountSwitcherComponent,
|
||||||
@@ -74,7 +75,6 @@ import { PasswordGeneratorHistoryComponent } from "./vault/password-generator-hi
|
|||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
HintComponent,
|
HintComponent,
|
||||||
LockComponent,
|
LockComponent,
|
||||||
LoginComponent,
|
|
||||||
NavComponent,
|
NavComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
PasswordGeneratorHistoryComponent,
|
PasswordGeneratorHistoryComponent,
|
||||||
@@ -100,6 +100,7 @@ import { PasswordGeneratorHistoryComponent } from "./vault/password-generator-hi
|
|||||||
VaultTimeoutInputComponent,
|
VaultTimeoutInputComponent,
|
||||||
ViewComponent,
|
ViewComponent,
|
||||||
ViewCustomFieldsComponent,
|
ViewCustomFieldsComponent,
|
||||||
|
LoginApprovalComponent,
|
||||||
],
|
],
|
||||||
bootstrap: [AppComponent],
|
bootstrap: [AppComponent],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2061,10 +2061,110 @@
|
|||||||
"logInWithAnotherDevice": {
|
"logInWithAnotherDevice": {
|
||||||
"message": "Log in with another device"
|
"message": "Log in with another device"
|
||||||
},
|
},
|
||||||
|
"logInInitiated": {
|
||||||
|
"message": "Log in initiated"
|
||||||
|
},
|
||||||
|
"notificationSentDevice": {
|
||||||
|
"message": "A notification has been sent to your device."
|
||||||
|
},
|
||||||
|
"fingerprintMatchInfo": {
|
||||||
|
"message": "Please make sure your vault is unlocked and Fingerprint phrase matches the other device."
|
||||||
|
},
|
||||||
|
"fingerprintPhraseHeader": {
|
||||||
|
"message": "Fingerprint phrase"
|
||||||
|
},
|
||||||
|
"needAnotherOption": {
|
||||||
|
"message": "Log in with device must be set up in the settings of the Bitwarden app. Need another option?"
|
||||||
|
},
|
||||||
|
"viewAllLoginOptions": {
|
||||||
|
"message": "View all login options"
|
||||||
|
},
|
||||||
|
"resendNotification": {
|
||||||
|
"message": "Resend notification"
|
||||||
|
},
|
||||||
"toggleCharacterCount": {
|
"toggleCharacterCount": {
|
||||||
"message": "Toggle character count",
|
"message": "Toggle character count",
|
||||||
"description": "'Character count' describes a feature that displays a number next to each character of the password."
|
"description": "'Character count' describes a feature that displays a number next to each character of the password."
|
||||||
},
|
},
|
||||||
|
"areYouTryingtoLogin": {
|
||||||
|
"message": "Are you trying to log in?"
|
||||||
|
},
|
||||||
|
"logInAttemptBy": {
|
||||||
|
"message": "Login attempt by $EMAIL$",
|
||||||
|
"placeholders": {
|
||||||
|
"email": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "name@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deviceType": {
|
||||||
|
"message": "Device Type"
|
||||||
|
},
|
||||||
|
"ipAddress": {
|
||||||
|
"message": "IP Address"
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"message": "Time"
|
||||||
|
},
|
||||||
|
"confirmLogIn": {
|
||||||
|
"message": "Confirm login"
|
||||||
|
},
|
||||||
|
"denyLogIn": {
|
||||||
|
"message": "Deny login"
|
||||||
|
},
|
||||||
|
"approveLoginRequests": {
|
||||||
|
"message": "Approve login requests"
|
||||||
|
},
|
||||||
|
"logInConfirmedForEmailOnDevice": {
|
||||||
|
"message": "Login confirmed for $EMAIL$ on $DEVICE$",
|
||||||
|
"placeholders": {
|
||||||
|
"email": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "name@example.com"
|
||||||
|
},
|
||||||
|
"device": {
|
||||||
|
"content": "$2",
|
||||||
|
"example": "iOS"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"youDeniedALogInAttemptFromAnotherDevice": {
|
||||||
|
"message": "You denied a login attempt from another device. If this really was you, try to log in with the device again."
|
||||||
|
},
|
||||||
|
"justNow": {
|
||||||
|
"message": "Just now"
|
||||||
|
},
|
||||||
|
"requestedXMinutesAgo": {
|
||||||
|
"message": "Requested $MINUTES$ minutes ago",
|
||||||
|
"placeholders": {
|
||||||
|
"minutes": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loginRequestHasAlreadyExpired": {
|
||||||
|
"message": "Login request has already expired."
|
||||||
|
},
|
||||||
|
"thisRequestIsNoLongerValid": {
|
||||||
|
"message": "This request is no longer valid."
|
||||||
|
},
|
||||||
|
"approveLoginRequestDesc": {
|
||||||
|
"message": "Use this device to approve login requests made from other devices."
|
||||||
|
},
|
||||||
|
"confirmLoginAtemptForMail": {
|
||||||
|
"message": "Confirm login attempt for $EMAIL$",
|
||||||
|
"placeholders": {
|
||||||
|
"email": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "name@example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"logInRequested": {
|
||||||
|
"message": "Log in requested"
|
||||||
|
},
|
||||||
"exposedMasterPassword": {
|
"exposedMasterPassword": {
|
||||||
"message": "Exposed Master Password"
|
"message": "Exposed Master Password"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
@import "variables.scss";
|
@import "variables.scss";
|
||||||
|
|
||||||
#login-page,
|
#login-page,
|
||||||
|
#login-with-device-page,
|
||||||
#lock-page,
|
#lock-page,
|
||||||
#sso-page,
|
#sso-page,
|
||||||
#set-password-page,
|
#set-password-page,
|
||||||
@@ -187,7 +188,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#login-page {
|
#login-page,
|
||||||
|
#login-with-device-page {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: unset;
|
justify-content: unset;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
@@ -216,3 +218,57 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#login-with-device-page {
|
||||||
|
.content {
|
||||||
|
display: block;
|
||||||
|
width: 350px !important;
|
||||||
|
|
||||||
|
.fingerprint {
|
||||||
|
margin: auto;
|
||||||
|
width: 315px;
|
||||||
|
|
||||||
|
.fingerpint-header {
|
||||||
|
padding-left: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.another-method {
|
||||||
|
display: flex;
|
||||||
|
margin: auto;
|
||||||
|
.description-text {
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("codeColor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#login-approval-page {
|
||||||
|
.section-title {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 16px;
|
||||||
|
.section {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
code {
|
||||||
|
@include themify($themes) {
|
||||||
|
color: themed("codeColor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h4.label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ $button-color: lighten($text-color, 40%);
|
|||||||
$button-color-primary: darken($brand-primary, 8%);
|
$button-color-primary: darken($brand-primary, 8%);
|
||||||
$button-color-danger: darken($brand-danger, 10%);
|
$button-color-danger: darken($brand-danger, 10%);
|
||||||
|
|
||||||
|
$code-color: #e83e8c;
|
||||||
|
|
||||||
$themes: (
|
$themes: (
|
||||||
light: (
|
light: (
|
||||||
textColor: $text-color,
|
textColor: $text-color,
|
||||||
@@ -95,6 +97,7 @@ $themes: (
|
|||||||
accountSwitcherTextColor: #ffffff,
|
accountSwitcherTextColor: #ffffff,
|
||||||
svgSuffix: "-light.svg",
|
svgSuffix: "-light.svg",
|
||||||
hrColor: #eeeeee,
|
hrColor: #eeeeee,
|
||||||
|
codeColor: $code-color,
|
||||||
),
|
),
|
||||||
dark: (
|
dark: (
|
||||||
textColor: #ffffff,
|
textColor: #ffffff,
|
||||||
@@ -150,6 +153,7 @@ $themes: (
|
|||||||
accountSwitcherTextColor: #ffffff,
|
accountSwitcherTextColor: #ffffff,
|
||||||
svgSuffix: "-dark.svg",
|
svgSuffix: "-dark.svg",
|
||||||
hrColor: #a3a3a3,
|
hrColor: #a3a3a3,
|
||||||
|
codeColor: $code-color,
|
||||||
),
|
),
|
||||||
nord: (
|
nord: (
|
||||||
textColor: $nord5,
|
textColor: $nord5,
|
||||||
@@ -205,6 +209,7 @@ $themes: (
|
|||||||
accountSwitcherTextColor: $nord5,
|
accountSwitcherTextColor: $nord5,
|
||||||
svgSuffix: "-dark.svg",
|
svgSuffix: "-dark.svg",
|
||||||
hrColor: $nord4,
|
hrColor: $nord4,
|
||||||
|
codeColor: $code-color,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, session } from "electron";
|
import * as path from "path";
|
||||||
|
|
||||||
|
import { app, dialog, ipcMain, Menu, MenuItem, nativeTheme, session, Notification } from "electron";
|
||||||
|
|
||||||
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
|
||||||
import { ThemeType } from "@bitwarden/common/enums/themeType";
|
import { ThemeType } from "@bitwarden/common/enums/themeType";
|
||||||
@@ -51,6 +53,21 @@ export class ElectronMainMessagingService implements MessagingService {
|
|||||||
return await session.defaultSession.cookies.get(options);
|
return await session.defaultSession.cookies.get(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("loginRequest", async (event, options) => {
|
||||||
|
const alert = new Notification({
|
||||||
|
title: options.alertTitle,
|
||||||
|
body: options.alertBody,
|
||||||
|
closeButtonText: options.buttonText,
|
||||||
|
icon: path.join(__dirname, "images/icon.png"),
|
||||||
|
});
|
||||||
|
|
||||||
|
alert.addListener("click", () => {
|
||||||
|
this.windowMain.win.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
alert.show();
|
||||||
|
});
|
||||||
|
|
||||||
nativeTheme.on("updated", () => {
|
nativeTheme.on("updated", () => {
|
||||||
windowMain.win?.webContents.send(
|
windowMain.win?.webContents.send(
|
||||||
"systemThemeUpdated",
|
"systemThemeUpdated",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { first } from "rxjs/operators";
|
|||||||
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
|
||||||
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
@@ -98,7 +99,8 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
private totpService: TotpService,
|
private totpService: TotpService,
|
||||||
private passwordRepromptService: PasswordRepromptService,
|
private passwordRepromptService: PasswordRepromptService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private searchBarService: SearchBarService
|
private searchBarService: SearchBarService,
|
||||||
|
private apiService: ApiService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -207,6 +209,16 @@ export class VaultComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.searchBarService.setEnabled(true);
|
this.searchBarService.setEnabled(true);
|
||||||
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
this.searchBarService.setPlaceholderText(this.i18nService.t("searchVault"));
|
||||||
|
|
||||||
|
const approveLoginRequests = await this.stateService.getApproveLoginRequests();
|
||||||
|
if (approveLoginRequests) {
|
||||||
|
const authRequest = await this.apiService.getLastAuthRequest();
|
||||||
|
if (authRequest != null) {
|
||||||
|
this.messagingService.send("openLoginApproval", {
|
||||||
|
notificationId: authRequest.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy() {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
import { Router } from "@angular/router";
|
||||||
import { Subject, takeUntil } from "rxjs";
|
|
||||||
|
|
||||||
import { CaptchaProtectedComponent } from "@bitwarden/angular/components/captchaProtected.component";
|
import { LoginWithDeviceComponent as BaseLoginWithDeviceComponent } from "@bitwarden/angular/components/login-with-device.component";
|
||||||
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||||
@@ -16,13 +15,6 @@ import { LoginService } from "@bitwarden/common/abstractions/login.service";
|
|||||||
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||||
import { AuthRequestType } from "@bitwarden/common/enums/authRequestType";
|
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
|
||||||
import { PasswordlessLogInCredentials } from "@bitwarden/common/models/domain/log-in-credentials";
|
|
||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
|
||||||
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/models/request/passwordless-create-auth.request";
|
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/models/response/auth-request.response";
|
|
||||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
|
||||||
|
|
||||||
import { StateService } from "../../core/state/state.service";
|
import { StateService } from "../../core/state/state.service";
|
||||||
|
|
||||||
@@ -31,175 +23,42 @@ import { StateService } from "../../core/state/state.service";
|
|||||||
templateUrl: "login-with-device.component.html",
|
templateUrl: "login-with-device.component.html",
|
||||||
})
|
})
|
||||||
export class LoginWithDeviceComponent
|
export class LoginWithDeviceComponent
|
||||||
extends CaptchaProtectedComponent
|
extends BaseLoginWithDeviceComponent
|
||||||
implements OnInit, OnDestroy
|
implements OnInit, OnDestroy
|
||||||
{
|
{
|
||||||
private destroy$ = new Subject<void>();
|
|
||||||
email: string;
|
|
||||||
showResendNotification = false;
|
|
||||||
passwordlessRequest: PasswordlessCreateAuthRequest;
|
|
||||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
|
||||||
onSuccessfulLogin: () => Promise<any>;
|
|
||||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
|
||||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
|
||||||
|
|
||||||
protected twoFactorRoute = "2fa";
|
|
||||||
protected successRoute = "vault";
|
|
||||||
protected forcePasswordResetRoute = "update-temp-password";
|
|
||||||
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
router: Router,
|
||||||
private cryptoService: CryptoService,
|
cryptoService: CryptoService,
|
||||||
private cryptoFunctionService: CryptoFunctionService,
|
cryptoFunctionService: CryptoFunctionService,
|
||||||
private appIdService: AppIdService,
|
appIdService: AppIdService,
|
||||||
private passwordGenerationService: PasswordGenerationService,
|
passwordGenerationService: PasswordGenerationService,
|
||||||
private apiService: ApiService,
|
apiService: ApiService,
|
||||||
private authService: AuthService,
|
authService: AuthService,
|
||||||
private logService: LogService,
|
logService: LogService,
|
||||||
environmentService: EnvironmentService,
|
environmentService: EnvironmentService,
|
||||||
i18nService: I18nService,
|
i18nService: I18nService,
|
||||||
platformUtilsService: PlatformUtilsService,
|
platformUtilsService: PlatformUtilsService,
|
||||||
private anonymousHubService: AnonymousHubService,
|
anonymousHubService: AnonymousHubService,
|
||||||
private validationService: ValidationService,
|
validationService: ValidationService,
|
||||||
private stateService: StateService,
|
stateService: StateService,
|
||||||
private loginService: LoginService
|
loginService: LoginService
|
||||||
) {
|
) {
|
||||||
super(environmentService, i18nService, platformUtilsService);
|
super(
|
||||||
|
router,
|
||||||
const navigation = this.router.getCurrentNavigation();
|
cryptoService,
|
||||||
if (navigation) {
|
cryptoFunctionService,
|
||||||
this.email = this.loginService.getEmail();
|
appIdService,
|
||||||
}
|
passwordGenerationService,
|
||||||
|
apiService,
|
||||||
//gets signalR push notification
|
authService,
|
||||||
this.authService
|
logService,
|
||||||
.getPushNotifcationObs$()
|
environmentService,
|
||||||
.pipe(takeUntil(this.destroy$))
|
i18nService,
|
||||||
.subscribe((id) => {
|
platformUtilsService,
|
||||||
this.confirmResponse(id);
|
anonymousHubService,
|
||||||
});
|
validationService,
|
||||||
}
|
stateService,
|
||||||
|
loginService
|
||||||
async ngOnInit() {
|
|
||||||
if (!this.email) {
|
|
||||||
this.router.navigate(["/login"]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.startPasswordlessLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
async startPasswordlessLogin() {
|
|
||||||
this.showResendNotification = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.buildAuthRequest();
|
|
||||||
const reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
|
|
||||||
|
|
||||||
if (reqResponse.id) {
|
|
||||||
this.anonymousHubService.createHubConnection(reqResponse.id);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
this.showResendNotification = true;
|
|
||||||
}, 12000);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.destroy$.next();
|
|
||||||
this.destroy$.complete();
|
|
||||||
this.anonymousHubService.stopHubConnection();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async confirmResponse(requestId: string) {
|
|
||||||
try {
|
|
||||||
const response = await this.apiService.getAuthResponse(
|
|
||||||
requestId,
|
|
||||||
this.passwordlessRequest.accessCode
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.requestApproved) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const credentials = await this.buildLoginCredntials(requestId, response);
|
|
||||||
const loginResponse = await this.authService.logIn(credentials);
|
|
||||||
|
|
||||||
if (loginResponse.requiresTwoFactor) {
|
|
||||||
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
|
||||||
this.onSuccessfulLoginTwoFactorNavigate();
|
|
||||||
} else {
|
|
||||||
this.router.navigate([this.twoFactorRoute]);
|
|
||||||
}
|
|
||||||
} else if (loginResponse.forcePasswordReset) {
|
|
||||||
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
|
||||||
this.onSuccessfulLoginForceResetNavigate();
|
|
||||||
} else {
|
|
||||||
this.router.navigate([this.forcePasswordResetRoute]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.loginService.saveEmailSettings();
|
|
||||||
if (this.onSuccessfulLogin != null) {
|
|
||||||
this.onSuccessfulLogin();
|
|
||||||
}
|
|
||||||
if (this.onSuccessfulLoginNavigate != null) {
|
|
||||||
this.onSuccessfulLoginNavigate();
|
|
||||||
} else {
|
|
||||||
this.router.navigate([this.successRoute]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ErrorResponse) {
|
|
||||||
this.router.navigate(["/login"]);
|
|
||||||
this.validationService.showError(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logService.error(error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async buildAuthRequest() {
|
|
||||||
this.authRequestKeyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
|
||||||
const fingerprint = await (
|
|
||||||
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair[0])
|
|
||||||
).join("-");
|
|
||||||
const deviceIdentifier = await this.appIdService.getAppId();
|
|
||||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair[0]);
|
|
||||||
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
|
|
||||||
|
|
||||||
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
|
|
||||||
this.email,
|
|
||||||
deviceIdentifier,
|
|
||||||
publicKey,
|
|
||||||
AuthRequestType.AuthenticateAndUnlock,
|
|
||||||
accessCode,
|
|
||||||
fingerprint
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async buildLoginCredntials(
|
|
||||||
requestId: string,
|
|
||||||
response: AuthRequestResponse
|
|
||||||
): Promise<PasswordlessLogInCredentials> {
|
|
||||||
const decKey = await this.cryptoService.rsaDecrypt(response.key, this.authRequestKeyPair[1]);
|
|
||||||
const decMasterPasswordHash = await this.cryptoService.rsaDecrypt(
|
|
||||||
response.masterPasswordHash,
|
|
||||||
this.authRequestKeyPair[1]
|
|
||||||
);
|
|
||||||
const key = new SymmetricCryptoKey(decKey);
|
|
||||||
const localHashedPassword = Utils.fromBufferToUtf8(decMasterPasswordHash);
|
|
||||||
|
|
||||||
return new PasswordlessLogInCredentials(
|
|
||||||
this.email,
|
|
||||||
this.passwordlessRequest.accessCode,
|
|
||||||
requestId,
|
|
||||||
key,
|
|
||||||
localHashedPassword
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,18 +209,6 @@ export class LoginComponent extends BaseLoginComponent implements OnInit, OnDest
|
|||||||
await super.submit(false);
|
await super.submit(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async startPasswordlessLogin() {
|
|
||||||
this.formGroup.get("masterPassword")?.clearValidators();
|
|
||||||
this.formGroup.get("masterPassword")?.updateValueAndValidity();
|
|
||||||
|
|
||||||
if (!this.formGroup.valid) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setFormValues();
|
|
||||||
this.router.navigate(["/login-with-device"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPasswordStrengthUserInput() {
|
private getPasswordStrengthUserInput() {
|
||||||
const email = this.formGroup.value.email;
|
const email = this.formGroup.value.email;
|
||||||
let userInput: string[] = [];
|
let userInput: string[] = [];
|
||||||
|
|||||||
210
libs/angular/src/components/login-with-device.component.ts
Normal file
210
libs/angular/src/components/login-with-device.component.ts
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
import { Directive, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { Router } from "@angular/router";
|
||||||
|
import { Subject, takeUntil } from "rxjs";
|
||||||
|
|
||||||
|
import { AnonymousHubService } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
|
||||||
|
import { AuthService } from "@bitwarden/common/abstractions/auth.service";
|
||||||
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
|
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
|
import { LoginService } from "@bitwarden/common/abstractions/login.service";
|
||||||
|
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
|
import { StateService } from "@bitwarden/common/abstractions/state.service";
|
||||||
|
import { ValidationService } from "@bitwarden/common/abstractions/validation.service";
|
||||||
|
import { AuthRequestType } from "@bitwarden/common/enums/authRequestType";
|
||||||
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
|
import { PasswordlessLogInCredentials } from "@bitwarden/common/models/domain/log-in-credentials";
|
||||||
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||||
|
import { PasswordlessCreateAuthRequest } from "@bitwarden/common/models/request/passwordless-create-auth.request";
|
||||||
|
import { AuthRequestResponse } from "@bitwarden/common/models/response/auth-request.response";
|
||||||
|
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
|
||||||
|
import { CaptchaProtectedComponent } from "./captchaProtected.component";
|
||||||
|
|
||||||
|
@Directive()
|
||||||
|
export class LoginWithDeviceComponent
|
||||||
|
extends CaptchaProtectedComponent
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
email: string;
|
||||||
|
showResendNotification = false;
|
||||||
|
passwordlessRequest: PasswordlessCreateAuthRequest;
|
||||||
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||||
|
onSuccessfulLogin: () => Promise<any>;
|
||||||
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
|
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||||
|
|
||||||
|
protected twoFactorRoute = "2fa";
|
||||||
|
protected successRoute = "vault";
|
||||||
|
protected forcePasswordResetRoute = "update-temp-password";
|
||||||
|
private resendTimeout = 12000;
|
||||||
|
private authRequestKeyPair: [publicKey: ArrayBuffer, privateKey: ArrayBuffer];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected router: Router,
|
||||||
|
private cryptoService: CryptoService,
|
||||||
|
private cryptoFunctionService: CryptoFunctionService,
|
||||||
|
private appIdService: AppIdService,
|
||||||
|
private passwordGenerationService: PasswordGenerationService,
|
||||||
|
private apiService: ApiService,
|
||||||
|
private authService: AuthService,
|
||||||
|
private logService: LogService,
|
||||||
|
environmentService: EnvironmentService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
platformUtilsService: PlatformUtilsService,
|
||||||
|
private anonymousHubService: AnonymousHubService,
|
||||||
|
private validationService: ValidationService,
|
||||||
|
private stateService: StateService,
|
||||||
|
private loginService: LoginService
|
||||||
|
) {
|
||||||
|
super(environmentService, i18nService, platformUtilsService);
|
||||||
|
|
||||||
|
const navigation = this.router.getCurrentNavigation();
|
||||||
|
if (navigation) {
|
||||||
|
this.email = navigation.extras?.state?.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
//gets signalR push notification
|
||||||
|
this.authService
|
||||||
|
.getPushNotifcationObs$()
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe((id) => {
|
||||||
|
this.confirmResponse(id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async ngOnInit() {
|
||||||
|
if (!this.email) {
|
||||||
|
this.router.navigate(["/login"]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.startPasswordlessLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
async startPasswordlessLogin() {
|
||||||
|
this.showResendNotification = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.buildAuthRequest();
|
||||||
|
const reqResponse = await this.apiService.postAuthRequest(this.passwordlessRequest);
|
||||||
|
|
||||||
|
if (reqResponse.id) {
|
||||||
|
this.anonymousHubService.createHubConnection(reqResponse.id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showResendNotification = true;
|
||||||
|
}, this.resendTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
this.anonymousHubService.stopHubConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async confirmResponse(requestId: string) {
|
||||||
|
try {
|
||||||
|
const response = await this.apiService.getAuthResponse(
|
||||||
|
requestId,
|
||||||
|
this.passwordlessRequest.accessCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.requestApproved) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = await this.buildLoginCredntials(requestId, response);
|
||||||
|
const loginResponse = await this.authService.logIn(credentials);
|
||||||
|
|
||||||
|
if (loginResponse.requiresTwoFactor) {
|
||||||
|
if (this.onSuccessfulLoginTwoFactorNavigate != null) {
|
||||||
|
this.onSuccessfulLoginTwoFactorNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.twoFactorRoute]);
|
||||||
|
}
|
||||||
|
} else if (loginResponse.forcePasswordReset) {
|
||||||
|
if (this.onSuccessfulLoginForceResetNavigate != null) {
|
||||||
|
this.onSuccessfulLoginForceResetNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.forcePasswordResetRoute]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await this.setRememberEmailValues();
|
||||||
|
if (this.onSuccessfulLogin != null) {
|
||||||
|
this.onSuccessfulLogin();
|
||||||
|
}
|
||||||
|
if (this.onSuccessfulLoginNavigate != null) {
|
||||||
|
this.onSuccessfulLoginNavigate();
|
||||||
|
} else {
|
||||||
|
this.router.navigate([this.successRoute]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof ErrorResponse) {
|
||||||
|
this.router.navigate(["/login"]);
|
||||||
|
this.validationService.showError(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logService.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async setRememberEmailValues() {
|
||||||
|
const rememberEmail = this.loginService.getRememberEmail();
|
||||||
|
const rememberedEmail = this.loginService.getEmail();
|
||||||
|
await this.stateService.setRememberedEmail(rememberEmail ? rememberedEmail : null);
|
||||||
|
this.loginService.clearValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildAuthRequest() {
|
||||||
|
this.authRequestKeyPair = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||||
|
const fingerprint = await (
|
||||||
|
await this.cryptoService.getFingerprint(this.email, this.authRequestKeyPair[0])
|
||||||
|
).join("-");
|
||||||
|
const deviceIdentifier = await this.appIdService.getAppId();
|
||||||
|
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair[0]);
|
||||||
|
const accessCode = await this.passwordGenerationService.generatePassword({ length: 25 });
|
||||||
|
|
||||||
|
this.passwordlessRequest = new PasswordlessCreateAuthRequest(
|
||||||
|
this.email,
|
||||||
|
deviceIdentifier,
|
||||||
|
publicKey,
|
||||||
|
AuthRequestType.AuthenticateAndUnlock,
|
||||||
|
accessCode,
|
||||||
|
fingerprint
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async buildLoginCredntials(
|
||||||
|
requestId: string,
|
||||||
|
response: AuthRequestResponse
|
||||||
|
): Promise<PasswordlessLogInCredentials> {
|
||||||
|
const decKey = await this.cryptoService.rsaDecrypt(response.key, this.authRequestKeyPair[1]);
|
||||||
|
const decMasterPasswordHash = await this.cryptoService.rsaDecrypt(
|
||||||
|
response.masterPasswordHash,
|
||||||
|
this.authRequestKeyPair[1]
|
||||||
|
);
|
||||||
|
const key = new SymmetricCryptoKey(decKey);
|
||||||
|
const localHashedPassword = Utils.fromBufferToUtf8(decMasterPasswordHash);
|
||||||
|
|
||||||
|
return new PasswordlessLogInCredentials(
|
||||||
|
this.email,
|
||||||
|
this.passwordlessRequest.accessCode,
|
||||||
|
requestId,
|
||||||
|
key,
|
||||||
|
localHashedPassword
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
|||||||
onSuccessfulLoginNavigate: () => Promise<any>;
|
onSuccessfulLoginNavigate: () => Promise<any>;
|
||||||
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
onSuccessfulLoginTwoFactorNavigate: () => Promise<any>;
|
||||||
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
onSuccessfulLoginForceResetNavigate: () => Promise<any>;
|
||||||
private selfHosted = false;
|
selfHosted = false;
|
||||||
showLoginWithDevice: boolean;
|
showLoginWithDevice: boolean;
|
||||||
validatedEmail = false;
|
validatedEmail = false;
|
||||||
paramEmailSet = false;
|
paramEmailSet = false;
|
||||||
@@ -176,6 +176,18 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async startPasswordlessLogin() {
|
||||||
|
this.formGroup.get("masterPassword")?.clearValidators();
|
||||||
|
this.formGroup.get("masterPassword")?.updateValueAndValidity();
|
||||||
|
|
||||||
|
if (!this.formGroup.valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const email = this.formGroup.get("email").value;
|
||||||
|
this.router.navigate(["/login-with-device"], { state: { email: email } });
|
||||||
|
}
|
||||||
|
|
||||||
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
async launchSsoBrowser(clientId: string, ssoRedirectUri: string) {
|
||||||
await this.saveEmailSettings();
|
await this.saveEmailSettings();
|
||||||
// Generate necessary sso params
|
// Generate necessary sso params
|
||||||
@@ -259,7 +271,7 @@ export class LoginComponent extends CaptchaProtectedComponent implements OnInit
|
|||||||
return `${error.controlName}${name}`;
|
return `${error.controlName}${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getLoginWithDevice(email: string) {
|
async getLoginWithDevice(email: string) {
|
||||||
try {
|
try {
|
||||||
const deviceIdentifier = await this.appIdService.getAppId();
|
const deviceIdentifier = await this.appIdService.getAppId();
|
||||||
const res = await this.apiService.getKnownDevice(email, deviceIdentifier);
|
const res = await this.apiService.getKnownDevice(email, deviceIdentifier);
|
||||||
|
|||||||
@@ -455,6 +455,7 @@ import { AbstractThemingService } from "./theming/theming.service.abstraction";
|
|||||||
LogService,
|
LogService,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
AuthServiceAbstraction,
|
AuthServiceAbstraction,
|
||||||
|
MessagingServiceAbstraction,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { OrganizationSponsorshipCreateRequest } from "../models/request/organiza
|
|||||||
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organization-sponsorship-redeem.request";
|
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organization-sponsorship-redeem.request";
|
||||||
import { PasswordHintRequest } from "../models/request/password-hint.request";
|
import { PasswordHintRequest } from "../models/request/password-hint.request";
|
||||||
import { PasswordRequest } from "../models/request/password.request";
|
import { PasswordRequest } from "../models/request/password.request";
|
||||||
|
import { PasswordlessAuthRequest } from "../models/request/passwordless-auth.request";
|
||||||
import { PasswordlessCreateAuthRequest } from "../models/request/passwordless-create-auth.request";
|
import { PasswordlessCreateAuthRequest } from "../models/request/passwordless-create-auth.request";
|
||||||
import { PaymentRequest } from "../models/request/payment.request";
|
import { PaymentRequest } from "../models/request/payment.request";
|
||||||
import { PreloginRequest } from "../models/request/prelogin.request";
|
import { PreloginRequest } from "../models/request/prelogin.request";
|
||||||
@@ -204,6 +205,10 @@ export abstract class ApiService {
|
|||||||
//passwordless
|
//passwordless
|
||||||
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
|
postAuthRequest: (request: PasswordlessCreateAuthRequest) => Promise<AuthRequestResponse>;
|
||||||
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
getAuthResponse: (id: string, accessCode: string) => Promise<AuthRequestResponse>;
|
||||||
|
getAuthRequest: (id: string) => Promise<AuthRequestResponse>;
|
||||||
|
putAuthRequest: (id: string, request: PasswordlessAuthRequest) => Promise<AuthRequestResponse>;
|
||||||
|
getAuthRequests: () => Promise<ListResponse<AuthRequestResponse>>;
|
||||||
|
getLastAuthRequest: () => Promise<AuthRequestResponse>;
|
||||||
|
|
||||||
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
|
getUserBillingHistory: () => Promise<BillingHistoryResponse>;
|
||||||
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
|
getUserBillingPayment: () => Promise<BillingPaymentResponse>;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "../models/domain/log-in-credentials";
|
} from "../models/domain/log-in-credentials";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||||
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
|
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
|
||||||
|
import { AuthRequestResponse } from "../models/response/auth-request.response";
|
||||||
import { AuthRequestPushNotification } from "../models/response/notification.response";
|
import { AuthRequestPushNotification } from "../models/response/notification.response";
|
||||||
|
|
||||||
export abstract class AuthService {
|
export abstract class AuthService {
|
||||||
@@ -37,6 +38,10 @@ export abstract class AuthService {
|
|||||||
authingWithPasswordless: () => boolean;
|
authingWithPasswordless: () => boolean;
|
||||||
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
|
getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
|
||||||
authResponsePushNotifiction: (notification: AuthRequestPushNotification) => Promise<any>;
|
authResponsePushNotifiction: (notification: AuthRequestPushNotification) => Promise<any>;
|
||||||
|
passwordlessLogin: (
|
||||||
|
id: string,
|
||||||
|
key: string,
|
||||||
|
requestApproved: boolean
|
||||||
|
) => Promise<AuthRequestResponse>;
|
||||||
getPushNotifcationObs$: () => Observable<any>;
|
getPushNotifcationObs$: () => Observable<any>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,4 +34,9 @@ export abstract class EnvironmentService {
|
|||||||
setUrls: (urls: Urls) => Promise<Urls>;
|
setUrls: (urls: Urls) => Promise<Urls>;
|
||||||
getUrls: () => Urls;
|
getUrls: () => Urls;
|
||||||
isCloud: () => boolean;
|
isCloud: () => boolean;
|
||||||
|
/**
|
||||||
|
* @remarks For desktop and browser use only.
|
||||||
|
* For web, use PlatformUtilsService.isSelfHost()
|
||||||
|
*/
|
||||||
|
isSelfHosted: () => boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -338,6 +338,8 @@ export abstract class StateService<T extends Account = Account> {
|
|||||||
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
setVaultTimeout: (value: number, options?: StorageOptions) => Promise<void>;
|
||||||
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
getVaultTimeoutAction: (options?: StorageOptions) => Promise<string>;
|
||||||
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
setVaultTimeoutAction: (value: string, options?: StorageOptions) => Promise<void>;
|
||||||
|
getApproveLoginRequests: (options?: StorageOptions) => Promise<boolean>;
|
||||||
|
setApproveLoginRequests: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||||
getStateVersion: () => Promise<number>;
|
getStateVersion: () => Promise<number>;
|
||||||
setStateVersion: (value: number) => Promise<void>;
|
setStateVersion: (value: number) => Promise<void>;
|
||||||
getWindow: () => Promise<WindowState>;
|
getWindow: () => Promise<WindowState>;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-types */
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
export type SharedFlags = {
|
export type SharedFlags = {
|
||||||
multithreadDecryption: boolean;
|
multithreadDecryption: boolean;
|
||||||
|
showPasswordless?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// required to avoid linting errors when there are no flags
|
// required to avoid linting errors when there are no flags
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export class AccountSettings {
|
|||||||
vaultTimeout?: number;
|
vaultTimeout?: number;
|
||||||
vaultTimeoutAction?: string = "lock";
|
vaultTimeoutAction?: string = "lock";
|
||||||
serverConfig?: ServerConfigData;
|
serverConfig?: ServerConfigData;
|
||||||
|
approveLoginRequests?: boolean;
|
||||||
avatarColor?: string;
|
avatarColor?: string;
|
||||||
|
|
||||||
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
static fromJSON(obj: Jsonify<AccountSettings>): AccountSettings {
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export class PasswordlessAuthRequest {
|
||||||
|
constructor(
|
||||||
|
readonly key: string,
|
||||||
|
readonly masterPasswordHash: string,
|
||||||
|
readonly deviceIdentifier: string,
|
||||||
|
readonly requestApproved: boolean
|
||||||
|
) {}
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import { DeviceType } from "../../enums/deviceType";
|
|||||||
|
|
||||||
import { BaseResponse } from "./base.response";
|
import { BaseResponse } from "./base.response";
|
||||||
|
|
||||||
|
const RequestTimeOut = 60000 * 15; //15 Minutes
|
||||||
|
|
||||||
export class AuthRequestResponse extends BaseResponse {
|
export class AuthRequestResponse extends BaseResponse {
|
||||||
id: string;
|
id: string;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
@@ -10,7 +12,11 @@ export class AuthRequestResponse extends BaseResponse {
|
|||||||
key: string;
|
key: string;
|
||||||
masterPasswordHash: string;
|
masterPasswordHash: string;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
requestApproved: boolean;
|
requestApproved?: boolean;
|
||||||
|
requestFingerprint?: string;
|
||||||
|
responseDate?: string;
|
||||||
|
isAnswered: boolean;
|
||||||
|
isExpired: boolean;
|
||||||
|
|
||||||
constructor(response: any) {
|
constructor(response: any) {
|
||||||
super(response);
|
super(response);
|
||||||
@@ -22,5 +28,32 @@ export class AuthRequestResponse extends BaseResponse {
|
|||||||
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
|
this.masterPasswordHash = this.getResponseProperty("MasterPasswordHash");
|
||||||
this.creationDate = this.getResponseProperty("CreationDate");
|
this.creationDate = this.getResponseProperty("CreationDate");
|
||||||
this.requestApproved = this.getResponseProperty("RequestApproved");
|
this.requestApproved = this.getResponseProperty("RequestApproved");
|
||||||
|
this.requestFingerprint = this.getResponseProperty("RequestFingerprint");
|
||||||
|
this.responseDate = this.getResponseProperty("ResponseDate");
|
||||||
|
|
||||||
|
const requestDate = new Date(this.creationDate);
|
||||||
|
const requestDateUTC = Date.UTC(
|
||||||
|
requestDate.getUTCFullYear(),
|
||||||
|
requestDate.getUTCMonth(),
|
||||||
|
requestDate.getDate(),
|
||||||
|
requestDate.getUTCHours(),
|
||||||
|
requestDate.getUTCMinutes(),
|
||||||
|
requestDate.getUTCSeconds(),
|
||||||
|
requestDate.getUTCMilliseconds()
|
||||||
|
);
|
||||||
|
|
||||||
|
const dateNow = new Date(Date.now());
|
||||||
|
const dateNowUTC = Date.UTC(
|
||||||
|
dateNow.getUTCFullYear(),
|
||||||
|
dateNow.getUTCMonth(),
|
||||||
|
dateNow.getDate(),
|
||||||
|
dateNow.getUTCHours(),
|
||||||
|
dateNow.getUTCMinutes(),
|
||||||
|
dateNow.getUTCSeconds(),
|
||||||
|
dateNow.getUTCMilliseconds()
|
||||||
|
);
|
||||||
|
|
||||||
|
this.isExpired = dateNowUTC - requestDateUTC >= RequestTimeOut;
|
||||||
|
this.isAnswered = this.requestApproved != null && this.responseDate != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { OrganizationSponsorshipCreateRequest } from "../models/request/organiza
|
|||||||
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organization-sponsorship-redeem.request";
|
import { OrganizationSponsorshipRedeemRequest } from "../models/request/organization/organization-sponsorship-redeem.request";
|
||||||
import { PasswordHintRequest } from "../models/request/password-hint.request";
|
import { PasswordHintRequest } from "../models/request/password-hint.request";
|
||||||
import { PasswordRequest } from "../models/request/password.request";
|
import { PasswordRequest } from "../models/request/password.request";
|
||||||
|
import { PasswordlessAuthRequest } from "../models/request/passwordless-auth.request";
|
||||||
import { PasswordlessCreateAuthRequest } from "../models/request/passwordless-create-auth.request";
|
import { PasswordlessCreateAuthRequest } from "../models/request/passwordless-create-auth.request";
|
||||||
import { PaymentRequest } from "../models/request/payment.request";
|
import { PaymentRequest } from "../models/request/payment.request";
|
||||||
import { PreloginRequest } from "../models/request/prelogin.request";
|
import { PreloginRequest } from "../models/request/prelogin.request";
|
||||||
@@ -266,6 +267,33 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
return new AuthRequestResponse(r);
|
return new AuthRequestResponse(r);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAuthRequest(id: string): Promise<AuthRequestResponse> {
|
||||||
|
const path = `/auth-requests/${id}`;
|
||||||
|
const r = await this.send("GET", path, null, true, true);
|
||||||
|
return new AuthRequestResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async putAuthRequest(id: string, request: PasswordlessAuthRequest): Promise<AuthRequestResponse> {
|
||||||
|
const path = `/auth-requests/${id}`;
|
||||||
|
const r = await this.send("PUT", path, request, true, true);
|
||||||
|
return new AuthRequestResponse(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthRequests(): Promise<ListResponse<AuthRequestResponse>> {
|
||||||
|
const path = `/auth-requests/`;
|
||||||
|
const r = await this.send("GET", path, null, true, true);
|
||||||
|
return new ListResponse(r, AuthRequestResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getLastAuthRequest(): Promise<AuthRequestResponse> {
|
||||||
|
const requests = await this.getAuthRequests();
|
||||||
|
const activeRequests = requests.data.filter((m) => !m.isAnswered && !m.isExpired);
|
||||||
|
const lastRequest = activeRequests.sort((a: AuthRequestResponse, b: AuthRequestResponse) =>
|
||||||
|
a.creationDate.localeCompare(b.creationDate)
|
||||||
|
)[activeRequests.length - 1];
|
||||||
|
return lastRequest;
|
||||||
|
}
|
||||||
|
|
||||||
// Account APIs
|
// Account APIs
|
||||||
|
|
||||||
async getProfile(): Promise<ProfileResponse> {
|
async getProfile(): Promise<ProfileResponse> {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { ApiService } from "../abstractions/api.service";
|
|||||||
import { AppIdService } from "../abstractions/appId.service";
|
import { AppIdService } from "../abstractions/appId.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||||
import { CryptoService } from "../abstractions/crypto.service";
|
import { CryptoService } from "../abstractions/crypto.service";
|
||||||
|
import { EncryptService } from "../abstractions/encrypt.service";
|
||||||
import { EnvironmentService } from "../abstractions/environment.service";
|
import { EnvironmentService } from "../abstractions/environment.service";
|
||||||
import { I18nService } from "../abstractions/i18n.service";
|
import { I18nService } from "../abstractions/i18n.service";
|
||||||
import { KeyConnectorService } from "../abstractions/keyConnector.service";
|
import { KeyConnectorService } from "../abstractions/keyConnector.service";
|
||||||
@@ -21,6 +22,7 @@ import { PasswordLogInStrategy } from "../misc/logInStrategies/passwordLogin.str
|
|||||||
import { PasswordlessLogInStrategy } from "../misc/logInStrategies/passwordlessLogin.strategy";
|
import { PasswordlessLogInStrategy } from "../misc/logInStrategies/passwordlessLogin.strategy";
|
||||||
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
import { SsoLogInStrategy } from "../misc/logInStrategies/ssoLogin.strategy";
|
||||||
import { UserApiLogInStrategy } from "../misc/logInStrategies/user-api-login.strategy";
|
import { UserApiLogInStrategy } from "../misc/logInStrategies/user-api-login.strategy";
|
||||||
|
import { Utils } from "../misc/utils";
|
||||||
import { AuthResult } from "../models/domain/auth-result";
|
import { AuthResult } from "../models/domain/auth-result";
|
||||||
import { KdfConfig } from "../models/domain/kdf-config";
|
import { KdfConfig } from "../models/domain/kdf-config";
|
||||||
import {
|
import {
|
||||||
@@ -31,7 +33,9 @@ import {
|
|||||||
} from "../models/domain/log-in-credentials";
|
} from "../models/domain/log-in-credentials";
|
||||||
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
||||||
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
|
import { TokenTwoFactorRequest } from "../models/request/identity-token/token-two-factor.request";
|
||||||
|
import { PasswordlessAuthRequest } from "../models/request/passwordless-auth.request";
|
||||||
import { PreloginRequest } from "../models/request/prelogin.request";
|
import { PreloginRequest } from "../models/request/prelogin.request";
|
||||||
|
import { AuthRequestResponse } from "../models/response/auth-request.response";
|
||||||
import { ErrorResponse } from "../models/response/error.response";
|
import { ErrorResponse } from "../models/response/error.response";
|
||||||
import { AuthRequestPushNotification } from "../models/response/notification.response";
|
import { AuthRequestPushNotification } from "../models/response/notification.response";
|
||||||
|
|
||||||
@@ -88,7 +92,8 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
protected environmentService: EnvironmentService,
|
protected environmentService: EnvironmentService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected twoFactorService: TwoFactorService,
|
protected twoFactorService: TwoFactorService,
|
||||||
protected i18nService: I18nService
|
protected i18nService: I18nService,
|
||||||
|
protected encryptService: EncryptService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async logIn(
|
async logIn(
|
||||||
@@ -275,6 +280,31 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
return this.pushNotificationSubject.asObservable();
|
return this.pushNotificationSubject.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async passwordlessLogin(
|
||||||
|
id: string,
|
||||||
|
key: string,
|
||||||
|
requestApproved: boolean
|
||||||
|
): Promise<AuthRequestResponse> {
|
||||||
|
const pubKey = Utils.fromB64ToArray(key);
|
||||||
|
const encryptedKey = await this.cryptoService.rsaEncrypt(
|
||||||
|
(
|
||||||
|
await this.cryptoService.getKey()
|
||||||
|
).encKey,
|
||||||
|
pubKey.buffer
|
||||||
|
);
|
||||||
|
const encryptedMasterPassword = await this.cryptoService.rsaEncrypt(
|
||||||
|
Utils.fromUtf8ToArray(await this.stateService.getKeyHash()),
|
||||||
|
pubKey.buffer
|
||||||
|
);
|
||||||
|
const request = new PasswordlessAuthRequest(
|
||||||
|
encryptedKey.encryptedString,
|
||||||
|
encryptedMasterPassword.encryptedString,
|
||||||
|
await this.appIdService.getAppId(),
|
||||||
|
requestApproved
|
||||||
|
);
|
||||||
|
return await this.apiService.putAuthRequest(id, request);
|
||||||
|
}
|
||||||
|
|
||||||
private saveState(
|
private saveState(
|
||||||
strategy:
|
strategy:
|
||||||
| UserApiLogInStrategy
|
| UserApiLogInStrategy
|
||||||
|
|||||||
@@ -213,4 +213,13 @@ export class EnvironmentService implements EnvironmentServiceAbstraction {
|
|||||||
this.getApiUrl()
|
this.getApiUrl()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isSelfHosted(): boolean {
|
||||||
|
return ![
|
||||||
|
"http://vault.bitwarden.com",
|
||||||
|
"https://vault.bitwarden.com",
|
||||||
|
"http://vault.qa.bitwarden.pw",
|
||||||
|
"https://vault.qa.bitwarden.pw",
|
||||||
|
].includes(this.getWebVaultUrl());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { AppIdService } from "../abstractions/appId.service";
|
|||||||
import { AuthService } from "../abstractions/auth.service";
|
import { AuthService } from "../abstractions/auth.service";
|
||||||
import { EnvironmentService } from "../abstractions/environment.service";
|
import { EnvironmentService } from "../abstractions/environment.service";
|
||||||
import { LogService } from "../abstractions/log.service";
|
import { LogService } from "../abstractions/log.service";
|
||||||
|
import { MessagingService } from "../abstractions/messaging.service";
|
||||||
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
import { NotificationsService as NotificationsServiceAbstraction } from "../abstractions/notifications.service";
|
||||||
import { StateService } from "../abstractions/state.service";
|
import { StateService } from "../abstractions/state.service";
|
||||||
import { AuthenticationStatus } from "../enums/authenticationStatus";
|
import { AuthenticationStatus } from "../enums/authenticationStatus";
|
||||||
@@ -34,7 +35,8 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
|||||||
private logoutCallback: (expired: boolean) => Promise<void>,
|
private logoutCallback: (expired: boolean) => Promise<void>,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private authService: AuthService
|
private authService: AuthService,
|
||||||
|
private messagingService: MessagingService
|
||||||
) {
|
) {
|
||||||
this.environmentService.urls.subscribe(() => {
|
this.environmentService.urls.subscribe(() => {
|
||||||
if (!this.inited) {
|
if (!this.inited) {
|
||||||
@@ -183,6 +185,13 @@ export class NotificationsService implements NotificationsServiceAbstraction {
|
|||||||
case NotificationType.SyncSendDelete:
|
case NotificationType.SyncSendDelete:
|
||||||
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
await this.syncService.syncDeleteSend(notification.payload as SyncSendNotification);
|
||||||
break;
|
break;
|
||||||
|
case NotificationType.AuthRequest:
|
||||||
|
if (await this.stateService.getApproveLoginRequests()) {
|
||||||
|
this.messagingService.send("openLoginApproval", {
|
||||||
|
notificationId: notification.payload.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2266,6 +2266,24 @@ export class StateService<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getApproveLoginRequests(options?: StorageOptions): Promise<boolean> {
|
||||||
|
const approveLoginRequests = (
|
||||||
|
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||||
|
)?.settings?.approveLoginRequests;
|
||||||
|
return approveLoginRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setApproveLoginRequests(value: boolean, options?: StorageOptions): Promise<void> {
|
||||||
|
const account = await this.getAccount(
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
|
);
|
||||||
|
account.settings.approveLoginRequests = value;
|
||||||
|
await this.saveAccount(
|
||||||
|
account,
|
||||||
|
this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async getStateVersion(): Promise<number> {
|
async getStateVersion(): Promise<number> {
|
||||||
return (await this.getGlobals(await this.defaultOnDiskLocalOptions())).stateVersion ?? 1;
|
return (await this.getGlobals(await this.defaultOnDiskLocalOptions())).stateVersion ?? 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user