1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00

Auth/ps 2298 reorg auth (#4564)

* Move auth service factories to Auth team

* Move authentication componenets to Auth team

* Move auth guard services to Auth team

* Move Duo content script to Auth team

* Move auth CLI commands to Auth team

* Move Desktop Account components to Auth Team

* Move Desktop guards to Auth team

* Move two-factor provider images to Auth team

* Move web Accounts components to Auth Team

* Move web settings components to Auth Team

* Move web two factor images to Auth Team

* Fix missed import changes for Auth Team

* Fix Linting errors

* Fix missed CLI imports

* Fix missed Desktop imports

* Revert images move

* Fix missed imports in Web

* Move angular lib components to Auth Team

* Move angular auth guards to Auth team

* Move strategy specs to Auth team

* Update .eslintignore for new paths

* Move lib common abstractions to Auth team

* Move services to Auth team

* Move common lib enums to Auth team

* Move webauthn iframe to Auth team

* Move lib common domain models to Auth team

* Move common lib requests to Auth team

* Move response models to Auth team

* Clean up whitelist

* Move bit web components to Auth team

* Move SSO and SCIM files to Auth team

* Revert move SCIM to Auth team

SCIM belongs to Admin Console team

* Move captcha to Auth team

* Move key connector to Auth team

* Move emergency access to auth team

* Delete extra file

* linter fixes

* Move kdf config to auth team

* Fix whitelist

* Fix duo autoformat

* Complete two factor provider request move

* Fix whitelist names

* Fix login capitalization

* Revert hint dependency reordering

* Revert hint dependency reordering

* Revert hint component

This components is being picked up as a move between clients

* Move web hint component to Auth team

* Move new files to auth team

* Fix desktop build

* Fix browser build
This commit is contained in:
Matt Gibson
2023-02-06 16:53:37 -05:00
committed by GitHub
parent 084c89107e
commit cf972e784c
377 changed files with 1030 additions and 998 deletions

View File

@@ -0,0 +1,34 @@
<form id="accessibility-cookie-page" #form [formGroup]="accessibilityForm" (ngSubmit)="submit()">
<div class="content">
<h1>{{ "loadAccessibilityCookie" | i18n }}</h1>
<p>
{{ "registerAccessibilityUser" | i18n }}
<a (click)="registerhCaptcha()">hcaptcha.com</a>.
{{ "copyPasteLink" | i18n }}
</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="link">{{ "hCaptchaUrl" | i18n }}</label>
<input
id="link"
type="text"
name="Link"
aria-describedby="linkHelp"
formControlName="link"
placeholder="{{ 'ex' | i18n }} https://accounts.hcaptcha.com/verify_email"
appAutofocus
appInputVerbatim
/>
</div>
</div>
<div id="linkHelp" class="box-footer">{{ "enterhCaptchaUrl" | i18n }}</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="!accessibilityForm.valid">
{{ "submit" | i18n }}
</button>
<button type="button" routerLink="/login" class="btn block">{{ "done" | i18n }}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,98 @@
import { Component, NgZone } from "@angular/core";
import { UntypedFormControl, UntypedFormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { Utils } from "@bitwarden/common/misc/utils";
import { getCookie } from "../utils";
const BroadcasterSubscriptionId = "AccessibilityCookieComponent";
@Component({
selector: "app-accessibility-cookie",
templateUrl: "accessibility-cookie.component.html",
})
export class AccessibilityCookieComponent {
listenForCookie = false;
hCaptchaWindow: Window;
accessibilityForm = new UntypedFormGroup({
link: new UntypedFormControl("", Validators.required),
});
constructor(
protected router: Router,
protected platformUtilsService: PlatformUtilsService,
protected environmentService: EnvironmentService,
protected i18nService: I18nService,
private broadcasterService: BroadcasterService,
protected ngZone: NgZone
) {}
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowIsFocused":
if (this.listenForCookie) {
this.listenForCookie = false;
this.checkForCookie();
}
break;
default:
}
});
});
}
registerhCaptcha() {
this.platformUtilsService.launchUri("https://www.hcaptcha.com/accessibility");
}
async checkForCookie() {
this.hCaptchaWindow.close();
const [cookie] = await getCookie("https://www.hcaptcha.com/", "hc_accessibility");
if (cookie) {
this.onCookieSavedSuccess();
} else {
this.onCookieSavedFailure();
}
}
onCookieSavedSuccess() {
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("accessibilityCookieSaved")
);
}
onCookieSavedFailure() {
this.platformUtilsService.showToast(
"error",
null,
this.i18nService.t("noAccessibilityCookieSaved")
);
}
async submit() {
if (Utils.getHostname(this.accessibilityForm.value.link) !== "accounts.hcaptcha.com") {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("invalidUrl")
);
return;
}
this.listenForCookie = true;
this.hCaptchaWindow = window.open(this.accessibilityForm.value.link);
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
}

View File

@@ -0,0 +1,43 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="deleteAccountTitle">
<div class="modal-dialog" role="document">
<form
class="modal-content"
#form
[appApiAction]="formPromise"
(ngSubmit)="submit()"
[formGroup]="deleteForm"
>
<div class="modal-body">
<p class="modal-text">{{ "deleteAccountDesc" | i18n }}</p>
<app-callout type="warning" title="{{ 'warning' | i18n }}">
{{ "deleteAccountWarning" | i18n }}
</app-callout>
<div class="box last">
<h1 class="box-header" id="deleteAccountTitle">
{{ "deleteAccount" | i18n }}
</h1>
<div class="box-content">
<app-user-verification
ngDefaultControl
formControlName="verification"
name="verification"
>
</app-user-verification>
</div>
<div id="confirmIdentityHelp" class="box-footer">
<p>{{ "confirmIdentity" | i18n }}</p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="danger" [disabled]="form.loading || !secret">
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
<span [hidden]="form.loading">{{ "deleteAccount" | i18n }}</span>
</button>
<button type="button" data-dismiss="modal" [disabled]="form.loading">
{{ "cancel" | i18n }}
</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,47 @@
import { Component } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { AccountApiService } from "@bitwarden/common/auth/abstractions/account-api.service";
import { Verification } from "@bitwarden/common/types/verification";
@Component({
selector: "app-delete-account",
templateUrl: "delete-account.component.html",
})
export class DeleteAccountComponent {
formPromise: Promise<void>;
deleteForm = this.formBuilder.group({
verification: undefined as Verification | undefined,
});
constructor(
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService,
private formBuilder: FormBuilder,
private accountApiService: AccountApiService,
private logService: LogService
) {}
get secret() {
return this.deleteForm.get("verification")?.value?.secret;
}
async submit() {
try {
const verification = this.deleteForm.get("verification").value;
this.formPromise = this.accountApiService.deleteAccount(verification);
await this.formPromise;
this.platformUtilsService.showToast(
"success",
this.i18nService.t("accountDeleted"),
this.i18nService.t("accountDeletedDesc")
);
} catch (e) {
this.logService.error(e);
}
}
}

View File

@@ -0,0 +1,103 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="selfHosteEnvironmentTitle">
<div class="modal-dialog" role="document">
<form class="modal-content" (ngSubmit)="submit()">
<div class="modal-body">
<div class="box">
<h1 class="box-header" id="selfHosteEnvironmentTitle">
{{ "selfHostedEnvironment" | i18n }}
</h1>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="baseUrl">{{ "baseUrl" | i18n }}</label>
<input
id="baseUrl"
type="text"
name="BaseUrl"
aria-describedby="baseUrlHelp"
[(ngModel)]="baseUrl"
placeholder="{{ 'ex' | i18n }} https://bitwarden.company.com"
appInputVerbatim
/>
</div>
</div>
<div id="baseUrlHelp" class="box-footer">
{{ "selfHostedEnvironmentFooter" | i18n }}
</div>
</div>
<div class="box">
<h2 class="box-header">
<button type="button" (click)="toggleCustom()" [attr.aria-expanded]="showCustom">
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-angle-right': !showCustom, 'bwi-angle-down': showCustom }"
></i>
{{ "customEnvironment" | i18n }}
</button>
</h2>
<div
role="group"
attr.aria-label="{{ 'customEnvironment' | i18n }}"
aria-describedby="customEnvironmentHelp"
class="box-content"
[hidden]="!showCustom"
>
<div class="box-content-row" appBoxRow>
<label for="webVaultUrl">{{ "webVaultUrl" | i18n }}</label>
<input
id="webVaultUrl"
type="text"
name="WebVaultUrl"
[(ngModel)]="webVaultUrl"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="apiUrl">{{ "apiUrl" | i18n }}</label>
<input id="apiUrl" type="text" name="ApiUrl" [(ngModel)]="apiUrl" appInputVerbatim />
</div>
<div class="box-content-row" appBoxRow>
<label for="identityUrl">{{ "identityUrl" | i18n }}</label>
<input
id="identityUrl"
type="text"
name="IdentityUrl"
[(ngModel)]="identityUrl"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="notificationsUrl">{{ "notificationsUrl" | i18n }}</label>
<input
id="notificationsUrl"
type="text"
name="NotificationsUrl"
[(ngModel)]="notificationsUrl"
appInputVerbatim
/>
</div>
<div class="box-content-row" appBoxRow>
<label for="iconsUrl">{{ "iconsUrl" | i18n }}</label>
<input
id="iconsUrl"
type="text"
name="IconsUrl"
[(ngModel)]="iconsUrl"
appInputVerbatim
/>
</div>
</div>
<div id="customEnvironmentHelp" class="box-footer" [hidden]="!showCustom">
{{ "customEnvironmentFooter" | i18n }}
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="primary" appA11yTitle="{{ 'save' | i18n }}">
<i class="bwi bwi-save-changes bwi-lg bwi-fw" aria-hidden="true"></i>
</button>
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
</div>
</form>
</div>
</div>

View File

@@ -0,0 +1,20 @@
import { Component } from "@angular/core";
import { EnvironmentComponent as BaseEnvironmentComponent } from "@bitwarden/angular/components/environment.component";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
@Component({
selector: "app-environment",
templateUrl: "environment.component.html",
})
export class EnvironmentComponent extends BaseEnvironmentComponent {
constructor(
platformUtilsService: PlatformUtilsService,
environmentService: EnvironmentService,
i18nService: I18nService
) {
super(platformUtilsService, environmentService, i18nService);
}
}

View File

@@ -0,0 +1,29 @@
import { Injectable } from "@angular/core";
import { CanActivate } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
const maxAllowedAccounts = 5;
@Injectable()
export class LoginGuard implements CanActivate {
protected homepage = "vault";
constructor(
private stateService: StateService,
private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService
) {}
async canActivate() {
const accounts = await firstValueFrom(this.stateService.accounts$);
if (accounts != null && Object.keys(accounts).length >= maxAllowedAccounts) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("accountLimitReached"));
return false;
}
return true;
}
}

View File

@@ -0,0 +1,76 @@
<form id="lock-page" (ngSubmit)="submit()">
<div class="content">
<p aria-hidden="true"><i class="bwi bwi-lock bwi-4x text-muted"></i></p>
<p>{{ "yourVaultIsLocked" | i18n }}</p>
<div class="box last">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow *ngIf="!hideInput">
<div class="row-main" *ngIf="pinLock">
<label for="pin">{{ "pin" | i18n }}</label>
<input
id="pin"
type="{{ showPassword ? 'text' : 'password' }}"
name="PIN"
class="monospaced"
[(ngModel)]="pin"
required
appInputVerbatim
/>
</div>
<div class="row-main" *ngIf="!pinLock">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
aria-describedby="masterPasswordHelp"
class="monospaced"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
{{ "loggedInAsOn" | i18n: email:webVaultHostname }}
</div>
</div>
<div class="buttons with-rows">
<div class="buttons-row" *ngIf="supportsBiometric && biometricLock">
<button
type="button"
class="btn block"
[ngClass]="{ 'primary font-weight-bold': hideInput }"
(click)="unlockBiometric()"
>
{{ biometricText | i18n }}
</button>
</div>
<div class="buttons-row">
<button type="submit" class="btn primary block" *ngIf="!hideInput">
<i class="bwi bwi-unlock" aria-hidden="true"></i> <b>{{ "unlock" | i18n }}</b>
</button>
<button type="button" class="btn block" (click)="logOut()">
{{ "logOut" | i18n }}
</button>
</div>
</div>
</div>
</form>

View File

@@ -0,0 +1,114 @@
import { Component, NgZone } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { ipcRenderer } from "electron";
import { LockComponent as BaseLockComponent } from "@bitwarden/angular/auth/components/lock.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.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 { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { VaultTimeoutService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeout.service";
import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vaultTimeout/vaultTimeoutSettings.service";
import { KeyConnectorService } from "@bitwarden/common/auth/abstractions/key-connector.service";
const BroadcasterSubscriptionId = "LockComponent";
@Component({
selector: "app-lock",
templateUrl: "lock.component.html",
})
export class LockComponent extends BaseLockComponent {
private deferFocus: boolean = null;
constructor(
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
messagingService: MessagingService,
cryptoService: CryptoService,
vaultTimeoutService: VaultTimeoutService,
vaultTimeoutSettingsService: VaultTimeoutSettingsService,
environmentService: EnvironmentService,
stateService: StateService,
apiService: ApiService,
private route: ActivatedRoute,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
logService: LogService,
keyConnectorService: KeyConnectorService
) {
super(
router,
i18nService,
platformUtilsService,
messagingService,
cryptoService,
vaultTimeoutService,
vaultTimeoutSettingsService,
environmentService,
stateService,
apiService,
logService,
keyConnectorService,
ngZone
);
}
async ngOnInit() {
await super.ngOnInit();
const autoPromptBiometric = !(await this.stateService.getNoAutoPromptBiometrics());
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.queryParams.subscribe((params) => {
setTimeout(async () => {
if (!params.promptBiometric || !this.supportsBiometric || !autoPromptBiometric) {
return;
}
if (await ipcRenderer.invoke("windowVisible")) {
this.unlockBiometric();
}
}, 1000);
});
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
}
ngOnDestroy() {
super.ngOnDestroy();
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
onWindowHidden() {
this.showPassword = false;
}
private focusInput() {
document.getElementById(this.pinLock ? "pin" : "masterPassword").focus();
}
}

View File

@@ -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>

View 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 { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthRequestResponse } from "@bitwarden/common/auth/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")
);
}
}
}

View File

@@ -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>

View File

@@ -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/auth/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 { 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 { 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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.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"]);
}
}

View File

@@ -0,0 +1,164 @@
<div id="login-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>
<form
id="login-page"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="formGroup"
attr.aria-hidden="{{ showingModal }}"
>
<div id="content" class="content">
<img class="logo-image" alt="Bitwarden" />
<p class="lead">{{ "loginOrCreateNewAccount" | i18n }}</p>
<!-- start email -->
<ng-container *ngIf="!validatedEmail; else loginPage">
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input
id="email"
type="email"
formControlName="email"
appInputVerbatim="false"
(keyup.enter)="continue()"
/>
</div>
</div>
<div class="box-footer" *ngIf="selfHostedDomain">
{{ "loggingInTo" | i18n: selfHostedDomain }}
</div>
</div>
<div class="checkbox remember-email">
<label for="rememberEmail">
<input
id="rememberEmail"
type="checkbox"
name="rememberEmail"
formControlName="rememberEmail"
/>
{{ "rememberEmail" | i18n }}
</label>
</div>
<div class="buttons with-rows">
<div class="buttons-row">
<button type="button" class="btn primary block" (click)="continue()">
{{ "continue" | i18n }}
</button>
</div>
</div>
<div class="sub-options">
<p class="no-margin">{{ "newAroundHere" | i18n }}</p>
<button type="button" class="text text-primary" routerLink="/register">
{{ "createAccount" | i18n }}
</button>
</div>
</ng-container>
<ng-template [formGroup]="formGroup" #loginPage>
<div class="box last">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPassword">{{ "masterPass" | i18n }}</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
class="monospaced"
formControlName="masterPassword"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box last" [hidden]="!showCaptcha()">
<div class="box-content">
<iframe id="hcaptcha_iframe" style="margin-top: 20px"></iframe>
<div class="box-content-row">
<button
class="btn block"
type="button"
routerLink="/accessibility-cookie"
(click)="setFormValues()"
>
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
{{ "loadAccessibilityCookie" | i18n }}
</button>
</div>
</div>
</div>
<div class="buttons with-rows">
<div class="buttons-row">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading"
><i class="bwi bwi-sign-in" aria-hidden="true"></i>
{{ "loginWithMasterPassword" | i18n }}</b
>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
</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">
<button
type="button"
(click)="launchSsoBrowser('desktop', 'bitwarden://sso-callback')"
class="btn block"
>
<i class="bwi bwi-provider" aria-hidden="true"></i>
{{ "enterpriseSingleSignOn" | i18n }}
</button>
</div>
</div>
<div class="sub-options">
<button
type="button"
class="text text-primary password-hint-btn"
routerLink="/hint"
(click)="setFormValues()"
>
{{ "getMasterPasswordHint" | i18n }}
</button>
<div>
<p class="no-margin">{{ "loggingInAs" | i18n }} {{ loggedEmail }}</p>
<a [routerLink]="[]" (click)="toggleValidateEmail(false)">{{ "notYou" | i18n }}</a>
</div>
</div>
</ng-template>
</div>
</form>
</div>
<ng-template #environment></ng-template>

View File

@@ -0,0 +1,186 @@
import { Component, NgZone, OnDestroy, ViewChild, ViewContainerRef } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { LoginComponent as BaseLoginComponent } from "@bitwarden/angular/auth/components/login.component";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoFunctionService } from "@bitwarden/common/abstractions/cryptoFunction.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { EnvironmentComponent } from "../environment.component";
const BroadcasterSubscriptionId = "LoginComponent";
@Component({
selector: "app-login",
templateUrl: "login.component.html",
})
export class LoginComponent extends BaseLoginComponent implements OnDestroy {
@ViewChild("environment", { read: ViewContainerRef, static: true })
environmentModal: ViewContainerRef;
webVaultHostname = "";
showingModal = false;
private deferFocus: boolean = null;
get loggedEmail() {
return this.formGroup.value.email;
}
get selfHostedDomain() {
return this.environmentService.hasBaseUrl() ? this.environmentService.getWebVaultUrl() : null;
}
constructor(
apiService: ApiService,
appIdService: AppIdService,
authService: AuthService,
router: Router,
i18nService: I18nService,
syncService: SyncService,
private modalService: ModalService,
platformUtilsService: PlatformUtilsService,
stateService: StateService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
cryptoFunctionService: CryptoFunctionService,
private broadcasterService: BroadcasterService,
ngZone: NgZone,
private messagingService: MessagingService,
logService: LogService,
formBuilder: FormBuilder,
formValidationErrorService: FormValidationErrorsService,
route: ActivatedRoute,
loginService: LoginService
) {
super(
apiService,
appIdService,
authService,
router,
platformUtilsService,
i18nService,
stateService,
environmentService,
passwordGenerationService,
cryptoFunctionService,
logService,
ngZone,
formBuilder,
formValidationErrorService,
route,
loginService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
}
async ngOnInit() {
await super.ngOnInit();
await this.checkSelfHosted();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
case "windowIsFocused":
if (this.deferFocus === null) {
this.deferFocus = !message.windowIsFocused;
if (!this.deferFocus) {
this.focusInput();
}
} else if (this.deferFocus && message.windowIsFocused) {
this.focusInput();
this.deferFocus = false;
}
break;
default:
}
});
});
this.messagingService.send("getWindowIsFocused");
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
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, rxjs/no-async-subscribe
childComponent.onSaved.subscribe(async () => {
modal.close();
await this.checkSelfHosted();
});
}
onWindowHidden() {
this.showPassword = false;
}
async continue() {
await super.validateEmail();
if (!this.formGroup.controls.email.valid) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccured"),
this.i18nService.t("invalidEmail")
);
return;
}
this.focusInput();
}
async submit() {
if (!this.validatedEmail) {
return;
}
await super.submit();
if (this.captchaSiteKey) {
const content = document.getElementById("content") as HTMLDivElement;
content.setAttribute("style", "width:335px");
}
}
private focusInput() {
const email = this.loggedEmail;
document.getElementById(email == null || email === "" ? "email" : "masterPassword").focus();
}
private async checkSelfHosted() {
this.selfHosted = this.environmentService.isSelfHosted();
await this.getLoginWithDevice(this.loggedEmail);
}
}

View File

@@ -0,0 +1,14 @@
import { NgModule } from "@angular/core";
import { RouterModule } from "@angular/router";
import { SharedModule } from "../../app/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 {}

View File

@@ -0,0 +1,146 @@
<form
id="register-page"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
[formGroup]="formGroup"
>
<div class="content">
<h1>{{ "createAccount" | i18n }}</h1>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="email">{{ "emailAddress" | i18n }}</label>
<input id="email" type="email" formControlName="email" appInputVerbatim />
</div>
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
class="monospaced"
aria-describedby="masterPasswordHelp"
formControlName="masterPassword"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<app-password-strength
[password]="formGroup.get('masterPassword')?.value"
[email]="formGroup.get('email')?.value"
[name]="formGroup.get('name')?.value"
(passwordStrengthResult)="getStrengthResult($event)"
(passwordScoreColor)="getPasswordScoreText($event)"
>
</app-password-strength>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
<b>{{ "important" | i18n }}</b> {{ "masterPasswordHint" | i18n }}
{{ characterMinimumMessage }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
class="monospaced"
formControlName="confirmMasterPassword"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword()"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" aria-describedby="hintHelp" formControlName="hint" />
</div>
<div class="box last" [hidden]="!showCaptcha()">
<div class="box-content">
<div class="box-content-row">
<iframe id="hcaptcha_iframe" height="80"></iframe>
<button class="btn block" type="button" routerLink="/accessibility-cookie">
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
{{ "loadAccessibilityCookie" | i18n }}
</button>
</div>
</div>
</div>
</div>
<div id="hintHelp" class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="box last">
<div class="box-footer checkbox">
<input type="checkbox" id="checkForBreaches" formControlName="checkForBreaches" />
<label for="checkForBreaches">
{{ "checkForBreaches" | i18n }}
</label>
</div>
<div class="box-footer checkbox" *ngIf="showTerms">
<input type="checkbox" id="acceptPolicies" formControlName="acceptPolicies" />
<label for="acceptPolicies">
{{ "acceptPolicies" | i18n }}<br />
<a href="https://bitwarden.com/terms/" target="_blank" rel="noopener">{{
"termsOfService" | i18n
}}</a
>,
<a href="https://bitwarden.com/privacy/" target="_blank" rel="noopener">{{
"privacyPolicy" | i18n
}}</a>
</label>
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading">{{ "submit" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" routerLink="/login" class="btn block">{{ "cancel" | i18n }}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,82 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { UntypedFormBuilder } from "@angular/forms";
import { Router } from "@angular/router";
import { RegisterComponent as BaseRegisterComponent } from "@bitwarden/angular/components/register.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { EnvironmentService } from "@bitwarden/common/abstractions/environment.service";
import { FormValidationErrorsService } from "@bitwarden/common/abstractions/formValidationErrors.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
const BroadcasterSubscriptionId = "RegisterComponent";
@Component({
selector: "app-register",
templateUrl: "register.component.html",
})
export class RegisterComponent extends BaseRegisterComponent implements OnInit, OnDestroy {
constructor(
formValidationErrorService: FormValidationErrorsService,
formBuilder: UntypedFormBuilder,
authService: AuthService,
router: Router,
i18nService: I18nService,
cryptoService: CryptoService,
apiService: ApiService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
environmentService: EnvironmentService,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
logService: LogService,
auditService: AuditService
) {
super(
formValidationErrorService,
formBuilder,
authService,
router,
i18nService,
cryptoService,
apiService,
stateService,
platformUtilsService,
passwordGenerationService,
environmentService,
logService,
auditService
);
}
async ngOnInit() {
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
default:
}
});
});
super.ngOnInit();
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
onWindowHidden() {
this.showPassword = false;
}
}

View File

@@ -0,0 +1,26 @@
<div id="remove-password-page" *ngIf="!loading">
<div class="content">
<h1>{{ "removeMasterPassword" | i18n }}</h1>
<p>{{ "convertOrganizationEncryptionDesc" | i18n: organization.name }}</p>
<div class="buttons">
<button
type="submit"
class="btn primary block"
[disabled]="actionPromise"
(click)="convert()"
>
<b [hidden]="continuing">{{ "removeMasterPassword" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!continuing" aria-hidden="true"></i>
</button>
<button
type="button"
class="btn secondary block"
[disabled]="actionPromise"
(click)="leave()"
>
<b [hidden]="leaving">{{ "leaveOrganization" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!leaving" aria-hidden="true"></i>
</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
import { Component } from "@angular/core";
import { RemovePasswordComponent as BaseRemovePasswordComponent } from "@bitwarden/angular/auth/components/remove-password.component";
@Component({
selector: "app-remove-password",
templateUrl: "remove-password.component.html",
})
export class RemovePasswordComponent extends BaseRemovePasswordComponent {}

View File

@@ -0,0 +1,418 @@
/**
* Duo Web SDK v2
* Copyright 2017, Duo Security
*/
var Duo;
(function (root, factory) {
// Browser globals (root is window)
var d = factory();
// If the Javascript was loaded via a script tag, attempt to autoload
// the frame.
d._onReady(d.init);
// Attach Duo to the `window` object
root.Duo = Duo = d;
}(window, function () {
var DUO_MESSAGE_FORMAT = /^(?:AUTH|ENROLL)+\|[A-Za-z0-9\+\/=]+\|[A-Za-z0-9\+\/=]+$/;
var DUO_ERROR_FORMAT = /^ERR\|[\w\s\.\(\)]+$/;
var DUO_OPEN_WINDOW_FORMAT = /^DUO_OPEN_WINDOW\|/;
var VALID_OPEN_WINDOW_DOMAINS = [
'duo.com',
'duosecurity.com',
'duomobile.s3-us-west-1.amazonaws.com'
];
var iframeId = 'duo_iframe',
postAction = '',
postArgument = 'sig_response',
host,
sigRequest,
duoSig,
appSig,
iframe,
submitCallback;
function throwError(message, url) {
throw new Error(
'Duo Web SDK error: ' + message +
(url ? ('\n' + 'See ' + url + ' for more information') : '')
);
}
function hyphenize(str) {
return str.replace(/([a-z])([A-Z])/, '$1-$2').toLowerCase();
}
// cross-browser data attributes
function getDataAttribute(element, name) {
if ('dataset' in element) {
return element.dataset[name];
} else {
return element.getAttribute('data-' + hyphenize(name));
}
}
// cross-browser event binding/unbinding
function on(context, event, fallbackEvent, callback) {
if ('addEventListener' in window) {
context.addEventListener(event, callback, false);
} else {
context.attachEvent(fallbackEvent, callback);
}
}
function off(context, event, fallbackEvent, callback) {
if ('removeEventListener' in window) {
context.removeEventListener(event, callback, false);
} else {
context.detachEvent(fallbackEvent, callback);
}
}
function onReady(callback) {
on(document, 'DOMContentLoaded', 'onreadystatechange', callback);
}
function offReady(callback) {
off(document, 'DOMContentLoaded', 'onreadystatechange', callback);
}
function onMessage(callback) {
on(window, 'message', 'onmessage', callback);
}
function offMessage(callback) {
off(window, 'message', 'onmessage', callback);
}
/**
* Parse the sig_request parameter, throwing errors if the token contains
* a server error or if the token is invalid.
*
* @param {String} sig Request token
*/
function parseSigRequest(sig) {
if (!sig) {
// nothing to do
return;
}
// see if the token contains an error, throwing it if it does
if (sig.indexOf('ERR|') === 0) {
throwError(sig.split('|')[1]);
}
// validate the token
if (sig.indexOf(':') === -1 || sig.split(':').length !== 2) {
throwError(
'Duo was given a bad token. This might indicate a configuration ' +
'problem with one of Duo\'s client libraries.',
'https://www.duosecurity.com/docs/duoweb#first-steps'
);
}
var sigParts = sig.split(':');
// hang on to the token, and the parsed duo and app sigs
sigRequest = sig;
duoSig = sigParts[0];
appSig = sigParts[1];
return {
sigRequest: sig,
duoSig: sigParts[0],
appSig: sigParts[1]
};
}
/**
* This function is set up to run when the DOM is ready, if the iframe was
* not available during `init`.
*/
function onDOMReady() {
iframe = document.getElementById(iframeId);
if (!iframe) {
throw new Error(
'This page does not contain an iframe for Duo to use.' +
'Add an element like <iframe id="duo_iframe"></iframe> ' +
'to this page. ' +
'See https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe ' +
'for more information.'
);
}
// we've got an iframe, away we go!
ready();
// always clean up after yourself
offReady(onDOMReady);
}
/**
* Validate that a MessageEvent came from the Duo service, and that it
* is a properly formatted payload.
*
* The Google Chrome sign-in page injects some JS into pages that also
* make use of postMessage, so we need to do additional validation above
* and beyond the origin.
*
* @param {MessageEvent} event Message received via postMessage
*/
function isDuoMessage(event) {
return Boolean(
event.origin === ('https://' + host) &&
typeof event.data === 'string' &&
(
event.data.match(DUO_MESSAGE_FORMAT) ||
event.data.match(DUO_ERROR_FORMAT) ||
event.data.match(DUO_OPEN_WINDOW_FORMAT)
)
);
}
/**
* Validate the request token and prepare for the iframe to become ready.
*
* All options below can be passed into an options hash to `Duo.init`, or
* specified on the iframe using `data-` attributes.
*
* Options specified using the options hash will take precedence over
* `data-` attributes.
*
* Example using options hash:
* ```javascript
* Duo.init({
* iframe: "some_other_id",
* host: "api-main.duo.test",
* sig_request: "...",
* post_action: "/auth",
* post_argument: "resp"
* });
* ```
*
* Example using `data-` attributes:
* ```
* <iframe id="duo_iframe"
* data-host="api-main.duo.test"
* data-sig-request="..."
* data-post-action="/auth"
* data-post-argument="resp"
* >
* </iframe>
* ```
*
* @param {Object} options
* @param {String} options.iframe The iframe, or id of an iframe to set up
* @param {String} options.host Hostname
* @param {String} options.sig_request Request token
* @param {String} [options.post_action=''] URL to POST back to after successful auth
* @param {String} [options.post_argument='sig_response'] Parameter name to use for response token
* @param {Function} [options.submit_callback] If provided, duo will not submit the form instead execute
* the callback function with reference to the "duo_form" form object
* submit_callback can be used to prevent the webpage from reloading.
*/
function init(options) {
if (options) {
if (options.host) {
host = options.host;
}
if (options.sig_request) {
parseSigRequest(options.sig_request);
}
if (options.post_action) {
postAction = options.post_action;
}
if (options.post_argument) {
postArgument = options.post_argument;
}
if (options.iframe) {
if (options.iframe.tagName) {
iframe = options.iframe;
} else if (typeof options.iframe === 'string') {
iframeId = options.iframe;
}
}
if (typeof options.submit_callback === 'function') {
submitCallback = options.submit_callback;
}
}
// if we were given an iframe, no need to wait for the rest of the DOM
if (false && iframe) {
ready();
} else {
// try to find the iframe in the DOM
iframe = document.getElementById(iframeId);
// iframe is in the DOM, away we go!
if (iframe) {
ready();
} else {
// wait until the DOM is ready, then try again
onReady(onDOMReady);
}
}
// always clean up after yourself!
offReady(init);
}
/**
* This function is called when a message was received from another domain
* using the `postMessage` API. Check that the event came from the Duo
* service domain, and that the message is a properly formatted payload,
* then perform the post back to the primary service.
*
* @param event Event object (contains origin and data)
*/
function onReceivedMessage(event) {
if (isDuoMessage(event)) {
if (event.data.match(DUO_OPEN_WINDOW_FORMAT)) {
var url = event.data.substring("DUO_OPEN_WINDOW|".length);
if (isValidUrlToOpen(url)) {
// Open the URL that comes after the DUO_WINDOW_OPEN token.
window.open(url, "_self");
}
}
else {
// the event came from duo, do the post back
doPostBack(event.data);
// always clean up after yourself!
offMessage(onReceivedMessage);
}
}
}
/**
* Validate that this passed in URL is one that we will actually allow to
* be opened.
* @param url String URL that the message poster wants to open
* @returns {boolean} true if we allow this url to be opened in the window
*/
function isValidUrlToOpen(url) {
if (!url) {
return false;
}
var parser = document.createElement('a');
parser.href = url;
if (parser.protocol === "duotrustedendpoints:") {
return true;
} else if (parser.protocol !== "https:") {
return false;
}
for (var i = 0; i < VALID_OPEN_WINDOW_DOMAINS.length; i++) {
if (parser.hostname.endsWith("." + VALID_OPEN_WINDOW_DOMAINS[i]) ||
parser.hostname === VALID_OPEN_WINDOW_DOMAINS[i]) {
return true;
}
}
return false;
}
/**
* Point the iframe at Duo, then wait for it to postMessage back to us.
*/
function ready() {
if (!host) {
host = getDataAttribute(iframe, 'host');
if (!host) {
throwError(
'No API hostname is given for Duo to use. Be sure to pass ' +
'a `host` parameter to Duo.init, or through the `data-host` ' +
'attribute on the iframe element.',
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
);
}
}
if (!duoSig || !appSig) {
parseSigRequest(getDataAttribute(iframe, 'sigRequest'));
if (!duoSig || !appSig) {
throwError(
'No valid signed request is given. Be sure to give the ' +
'`sig_request` parameter to Duo.init, or use the ' +
'`data-sig-request` attribute on the iframe element.',
'https://www.duosecurity.com/docs/duoweb#3.-show-the-iframe'
);
}
}
// if postAction/Argument are defaults, see if they are specified
// as data attributes on the iframe
if (postAction === '') {
postAction = getDataAttribute(iframe, 'postAction') || postAction;
}
if (postArgument === 'sig_response') {
postArgument = getDataAttribute(iframe, 'postArgument') || postArgument;
}
// point the iframe at Duo
iframe.src = [
'https://', host, '/frame/web/v1/auth?tx=', duoSig,
'&parent=', encodeURIComponent(document.location.href),
'&v=2.6'
].join('');
// listen for the 'message' event
onMessage(onReceivedMessage);
}
/**
* We received a postMessage from Duo. POST back to the primary service
* with the response token, and any additional user-supplied parameters
* given in form#duo_form.
*/
function doPostBack(response) {
// create a hidden input to contain the response token
var input = document.createElement('input');
input.type = 'hidden';
input.name = postArgument;
input.value = response + ':' + appSig;
// user may supply their own form with additional inputs
var form = document.getElementById('duo_form');
// if the form doesn't exist, create one
if (!form) {
form = document.createElement('form');
// insert the new form after the iframe
iframe.parentElement.insertBefore(form, iframe.nextSibling);
}
// make sure we are actually posting to the right place
form.method = 'POST';
form.action = postAction;
// add the response token input to the form
form.appendChild(input);
// away we go!
if (typeof submitCallback === "function") {
submitCallback.call(null, form);
} else {
form.submit();
}
}
return {
init: init,
_onReady: onReady,
_parseSigRequest: parseSigRequest,
_isDuoMessage: isDuoMessage,
_doPostBack: doPostBack
};
}));

View File

@@ -0,0 +1,156 @@
<form id="set-password-page" #form>
<div class="content">
<img class="logo-image" alt="Bitwarden" />
<p class="lead">{{ "setMasterPassword" | i18n }}</p>
<div class="box text-center" *ngIf="syncLoading">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<div *ngIf="!syncLoading">
<div class="box">
<app-callout type="tip">{{ "ssoCompleteRegistration" | i18n }}</app-callout>
<app-callout
type="warning"
title="{{ 'resetPasswordPolicyAutoEnroll' | i18n }}"
*ngIf="resetPasswordAutoEnroll"
>
{{ "resetPasswordAutoEnrollInviteWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
</div>
<form
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
ngNativeValidate
autocomplete="off"
>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword"
>{{ "masterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
aria-describedby="masterPasswordHelp"
[(ngModel)]="masterPassword"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<app-password-strength
[password]="masterPassword"
[email]="email"
(passwordStrengthResult)="getStrengthResult($event)"
(passwordScoreColor)="getPasswordScoreText($event)"
>
</app-password-strength>
</div>
</div>
<div id="masterPasswordHelp" class="box-footer">
{{ "masterPassDesc" | i18n }}
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="password"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
autocomplete="new-password"
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
</div>
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input
id="hint"
type="text"
name="Hint"
aria-describedby="hintHelp"
[(ngModel)]="hint"
/>
</div>
</div>
<div id="hintHelp" class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<i
*ngIf="form.loading"
class="bwi bwi-spinner bwi-spin"
title="{{ 'loading' | i18n }}"
aria-hidden="true"
></i>
<span>{{ "submit" | i18n }}</span>
</button>
<button type="button" class="btn block" (click)="logOut()">
<span>{{ "logOut" | i18n }}</span>
</button>
</div>
</form>
</div>
</div>
</form>

View File

@@ -0,0 +1,83 @@
import { Component, NgZone, OnDestroy } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/components/set-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
const BroadcasterSubscriptionId = "SetPasswordComponent";
@Component({
selector: "app-set-password",
templateUrl: "set-password.component.html",
})
export class SetPasswordComponent extends BaseSetPasswordComponent implements OnDestroy {
constructor(
apiService: ApiService,
i18nService: I18nService,
cryptoService: CryptoService,
messagingService: MessagingService,
passwordGenerationService: PasswordGenerationService,
platformUtilsService: PlatformUtilsService,
policyApiService: PolicyApiServiceAbstraction,
policyService: PolicyService,
router: Router,
syncService: SyncService,
route: ActivatedRoute,
private broadcasterService: BroadcasterService,
private ngZone: NgZone,
stateService: StateService,
organizationApiService: OrganizationApiServiceAbstraction,
organizationUserService: OrganizationUserService
) {
super(
i18nService,
cryptoService,
messagingService,
passwordGenerationService,
platformUtilsService,
policyApiService,
policyService,
router,
apiService,
syncService,
route,
stateService,
organizationApiService,
organizationUserService
);
}
async ngOnInit() {
await super.ngOnInit();
this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message) => {
this.ngZone.run(() => {
switch (message.command) {
case "windowHidden":
this.onWindowHidden();
break;
default:
}
});
});
}
ngOnDestroy() {
this.broadcasterService.unsubscribe(BroadcasterSubscriptionId);
}
onWindowHidden() {
this.showPassword = false;
}
}

View File

@@ -0,0 +1,9 @@
<form id="sso-page" (ngSubmit)="submit()">
<div class="content">
<img class="logo-image" alt="Bitwarden" />
<div class="box">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
</div>
</form>

View File

@@ -0,0 +1,54 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { ApiService } from "@bitwarden/common/abstractions/api.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 { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "app-sso",
templateUrl: "sso.component.html",
})
export class SsoComponent extends BaseSsoComponent {
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
syncService: SyncService,
route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationService,
logService: LogService
) {
super(
authService,
router,
i18nService,
route,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService
);
super.onSuccessfulLogin = () => {
return syncService.fullSync(true);
};
this.redirectUri = "bitwarden://sso-callback";
this.clientId = "desktop";
}
}

View File

@@ -0,0 +1,33 @@
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="twoStepTitle">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-body">
<div class="box">
<h1 class="box-header" id="twoStepTitle">
{{ "twoStepOptions" | i18n }}
</h1>
<div class="box-content">
<button
type="button"
appStopClick
*ngFor="let p of providers"
class="box-content-row"
(click)="choose(p)"
>
<img [src]="'images/two-factor/' + p.type + '.png'" alt="" class="img-right" />
<span class="text">{{ p.name }}</span>
<span class="detail">{{ p.description }}</span>
</button>
<button type="button" appStopClick class="box-content-row" (click)="recover()">
<span class="text">{{ "recoveryCodeTitle" | i18n }}</span>
<span class="detail">{{ "recoveryCodeDesc" | i18n }}</span>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" data-dismiss="modal">{{ "close" | i18n }}</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,22 @@
import { Component } from "@angular/core";
import { Router } from "@angular/router";
import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
@Component({
selector: "app-two-factor-options",
templateUrl: "two-factor-options.component.html",
})
export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
constructor(
twoFactorService: TwoFactorService,
router: Router,
i18nService: I18nService,
platformUtilsService: PlatformUtilsService
) {
super(twoFactorService, router, i18nService, platformUtilsService, window);
}
}

View File

@@ -0,0 +1,148 @@
<form
id="two-factor-page"
#form
(ngSubmit)="submit()"
[appApiAction]="formPromise"
attr.aria-hidden="{{ showingModal }}"
>
<div id="content" class="content">
<h1>{{ title }}</h1>
<p *ngIf="selectedProviderType === providerType.Authenticator">
{{ "enterVerificationCodeApp" | i18n }}
</p>
<p *ngIf="selectedProviderType === providerType.Email">
{{ "enterVerificationCodeEmail" | i18n: twoFactorEmail }}
</p>
<div
class="box last"
*ngIf="
selectedProviderType === providerType.Email ||
selectedProviderType === providerType.Authenticator
"
>
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="code">{{ "verificationCode" | i18n }}</label>
<input
id="code"
type="text"
name="Code"
[(ngModel)]="token"
required
appAutofocus
appInputVerbatim
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
<p>{{ "insertYubiKey" | i18n }}</p>
<img src="../../images/yubikey.jpg" alt="" />
<div class="box last">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="code" class="sr-only">{{ "verificationCode" | i18n }}</label>
<input
id="code"
type="password"
name="Code"
[(ngModel)]="token"
required
appAutofocus
appInputVerbatim
/>
</div>
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
</ng-container>
<ng-container *ngIf="selectedProviderType === providerType.WebAuthn">
<div id="web-authn-frame">
<iframe id="webauthn_iframe" [attr.allow]="webAuthnAllow"></iframe>
</div>
<div class="box first">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
</ng-container>
<ng-container
*ngIf="
selectedProviderType === providerType.Duo ||
selectedProviderType === providerType.OrganizationDuo
"
>
<div id="duo-frame"><iframe id="duo_iframe"></iframe></div>
<div class="box last">
<div class="box-content">
<div class="box-content-row box-content-row-checkbox" appBoxRow>
<label for="remember">{{ "rememberMe" | i18n }}</label>
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember" />
</div>
</div>
</div>
</ng-container>
<div class="box last" *ngIf="selectedProviderType == null">
<div class="box-content">
<div class="box-content-row">
<p>{{ "noTwoStepProviders" | i18n }}</p>
<p>{{ "noTwoStepProviders2" | i18n }}</p>
</div>
</div>
</div>
<div class="box last" [hidden]="!showCaptcha()">
<div class="box-content">
<div class="box-content-row">
<iframe id="hcaptcha_iframe" height="80"></iframe>
<button class="btn block" type="button" routerLink="/accessibility-cookie">
<i class="bwi bwi-universal-access" aria-hidden="true"></i>
{{ "loadAccessibilityCookie" | i18n }}
</button>
</div>
</div>
</div>
<div class="buttons">
<button
type="submit"
class="btn primary block"
[disabled]="form.loading"
*ngIf="
selectedProviderType != null &&
selectedProviderType !== providerType.Duo &&
selectedProviderType !== providerType.OrganizationDuo
"
>
<span [hidden]="form.loading"
><i class="bwi bwi-sign-in" aria-hidden="true"></i> {{ "continue" | i18n }}</span
>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" routerLink="/login" class="btn block">{{ "cancel" | i18n }}</button>
</div>
<div class="sub-options">
<button type="button" appStopClick (click)="anotherMethod()">
{{ "useAnotherTwoStepMethod" | i18n }}
</button>
<button
type="button"
appStopClick
(click)="sendEmail(true)"
[appApiAction]="emailPromise"
*ngIf="selectedProviderType === providerType.Email"
>
{{ "sendVerificationCodeEmailAgain" | i18n }}
</button>
</div>
</div>
</form>
<ng-template #twoFactorOptions></ng-template>

View File

@@ -0,0 +1,103 @@
import { Component, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AppIdService } from "@bitwarden/common/abstractions/appId.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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { LoginService } from "@bitwarden/common/auth/abstractions/login.service";
import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { TwoFactorOptionsComponent } from "./two-factor-options.component";
@Component({
selector: "app-two-factor",
templateUrl: "two-factor.component.html",
})
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
export class TwoFactorComponent extends BaseTwoFactorComponent {
@ViewChild("twoFactorOptions", { read: ViewContainerRef, static: true })
twoFactorOptionsModal: ViewContainerRef;
showingModal = false;
constructor(
authService: AuthService,
router: Router,
i18nService: I18nService,
apiService: ApiService,
platformUtilsService: PlatformUtilsService,
syncService: SyncService,
environmentService: EnvironmentService,
private modalService: ModalService,
stateService: StateService,
route: ActivatedRoute,
logService: LogService,
twoFactorService: TwoFactorService,
appIdService: AppIdService,
loginService: LoginService
) {
super(
authService,
router,
i18nService,
apiService,
platformUtilsService,
window,
environmentService,
stateService,
route,
logService,
twoFactorService,
appIdService,
loginService
);
super.onSuccessfulLogin = () => {
this.loginService.clearValues();
return syncService.fullSync(true);
};
}
async anotherMethod() {
const [modal, childComponent] = await this.modalService.openViewRef(
TwoFactorOptionsComponent,
this.twoFactorOptionsModal
);
// 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, rxjs/no-async-subscribe
childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => {
modal.close();
this.selectedProviderType = provider;
await this.init();
});
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
childComponent.onRecoverSelected.subscribe(() => {
modal.close();
});
}
async submit() {
await super.submit();
if (this.captchaSiteKey) {
const content = document.getElementById("content") as HTMLDivElement;
content.setAttribute("style", "width:335px");
}
}
}

View File

@@ -0,0 +1,114 @@
<form id="update-temp-password-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
<div class="content">
<app-callout type="warning" title="{{ 'updateMasterPassword' | i18n }}">
{{ "updateMasterPasswordWarning" | i18n }}
</app-callout>
<app-callout
type="info"
[enforcedPolicyOptions]="enforcedPolicyOptions"
*ngIf="enforcedPolicyOptions"
>
</app-callout>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<div class="box-content-row-flex">
<div class="row-main">
<label for="masterPassword">
{{ "masterPass" | i18n }}
<strong class="sub-label text-{{ color }}" *ngIf="text">
{{ text }}
</strong>
</label>
<input
id="masterPassword"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPassword"
class="monospaced"
[(ngModel)]="masterPassword"
required
[appAutofocus]="masterPassword === ''"
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(false)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
<app-password-strength
[password]="masterPassword"
[email]="email"
(passwordStrengthResult)="getStrengthResult($event)"
(passwordScoreColor)="getPasswordScoreText($event)"
>
</app-password-strength>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row box-content-row-flex" appBoxRow>
<div class="row-main">
<label for="masterPasswordRetype">{{ "reTypeMasterPass" | i18n }}</label>
<input
id="masterPasswordRetype"
type="{{ showPassword ? 'text' : 'password' }}"
name="MasterPasswordRetype"
class="monospaced"
[(ngModel)]="masterPasswordRetype"
required
appInputVerbatim
/>
</div>
<div class="action-buttons">
<button
type="button"
class="row-btn"
appStopClick
appA11yTitle="{{ 'toggleVisibility' | i18n }}"
[attr.aria-pressed]="showPassword"
(click)="togglePassword(true)"
>
<i
class="bwi bwi-lg"
aria-hidden="true"
[ngClass]="{ 'bwi-eye': !showPassword, 'bwi-eye-slash': showPassword }"
></i>
</button>
</div>
</div>
</div>
</div>
<div class="box">
<div class="box-content">
<div class="box-content-row" appBoxRow>
<label for="hint">{{ "masterPassHint" | i18n }}</label>
<input id="hint" type="text" name="Hint" aria-describedby="hintHelp" [(ngModel)]="hint" />
</div>
</div>
<div id="hintHelp" class="box-footer">
{{ "masterPassHintDesc" | i18n }}
</div>
</div>
<div class="buttons">
<button type="submit" class="btn primary block" [disabled]="form.loading">
<b [hidden]="form.loading">{{ "submit" | i18n }}</b>
<i class="bwi bwi-spinner bwi-spin" [hidden]="!form.loading" aria-hidden="true"></i>
</button>
<button type="button" (click)="logOut()" class="btn block">{{ "logOut" | i18n }}</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,45 @@
import { Component } from "@angular/core";
import { UpdateTempPasswordComponent as BaseUpdateTempPasswordComponent } from "@bitwarden/angular/auth/components/update-temp-password.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { MessagingService } from "@bitwarden/common/abstractions/messaging.service";
import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
import { StateService } from "@bitwarden/common/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@Component({
selector: "app-update-temp-password",
templateUrl: "update-temp-password.component.html",
})
export class UpdateTempPasswordComponent extends BaseUpdateTempPasswordComponent {
constructor(
i18nService: I18nService,
platformUtilsService: PlatformUtilsService,
passwordGenerationService: PasswordGenerationService,
policyService: PolicyService,
cryptoService: CryptoService,
messagingService: MessagingService,
apiService: ApiService,
syncService: SyncService,
logService: LogService,
stateService: StateService
) {
super(
i18nService,
platformUtilsService,
passwordGenerationService,
policyService,
cryptoService,
messagingService,
apiService,
stateService,
syncService,
logService
);
}
}