1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-19 02:44:01 +00:00
Files
browser/apps/web/src/app/tools/send/send-access/send-auth.component.ts
2026-02-17 09:52:23 -08:00

232 lines
8.2 KiB
TypeScript

import { ChangeDetectionStrategy, Component, input, OnInit, output, signal } from "@angular/core";
import { FormBuilder } from "@angular/forms";
import { firstValueFrom } from "rxjs";
import {
emailAndOtpRequired,
emailRequired,
otpInvalid,
passwordHashB64Invalid,
passwordHashB64Required,
SendAccessDomainCredentials,
SendAccessToken,
SendHashedPasswordB64,
sendIdInvalid,
SendOtp,
SendTokenService,
} from "@bitwarden/common/auth/send-access";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SendAccessRequest } from "@bitwarden/common/tools/send/models/request/send-access.request";
import { SendAccessResponse } from "@bitwarden/common/tools/send/models/response/send-access.response";
import { SEND_KDF_ITERATIONS } from "@bitwarden/common/tools/send/send-kdf";
import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction";
import { AuthType } from "@bitwarden/common/tools/send/types/auth-type";
import { AnonLayoutWrapperDataService, ToastService } from "@bitwarden/components";
import { SharedModule } from "../../../shared";
import { SendAccessEmailComponent } from "./send-access-email.component";
import { SendAccessPasswordComponent } from "./send-access-password.component";
@Component({
selector: "app-send-auth",
templateUrl: "send-auth.component.html",
imports: [SendAccessPasswordComponent, SendAccessEmailComponent, SharedModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SendAuthComponent implements OnInit {
protected readonly id = input.required<string>();
protected readonly key = input.required<string>();
protected accessGranted = output<{
response?: SendAccessResponse;
request?: SendAccessRequest;
accessToken?: SendAccessToken;
}>();
authType = AuthType;
private expiredAuthAttempts = 0;
readonly loading = signal<boolean>(false);
readonly error = signal<boolean>(false);
readonly unavailable = signal<boolean>(false);
readonly sendAuthType = signal<AuthType>(AuthType.None);
readonly enterOtp = signal<boolean>(false);
sendAccessForm = this.formBuilder.group<{ password?: string; email?: string; otp?: string }>({});
constructor(
private cryptoFunctionService: CryptoFunctionService,
private sendApiService: SendApiService,
private toastService: ToastService,
private i18nService: I18nService,
private formBuilder: FormBuilder,
private configService: ConfigService,
private sendTokenService: SendTokenService,
private anonLayoutWrapperDataService: AnonLayoutWrapperDataService,
) {}
ngOnInit() {
this.updatePageTitle();
void this.onSubmit();
}
async onSubmit() {
this.loading.set(true);
this.unavailable.set(false);
this.error.set(false);
const sendEmailOtp = await this.configService.getFeatureFlag(FeatureFlag.SendEmailOTP);
if (sendEmailOtp) {
await this.attemptV2Access();
} else {
await this.attemptV1Access();
}
this.loading.set(false);
}
private async attemptV1Access() {
try {
const accessRequest = new SendAccessRequest();
if (this.sendAuthType() === AuthType.Password) {
const password = this.sendAccessForm.value.password;
if (password == null) {
return;
}
accessRequest.password = await this.getPasswordHashB64(password, this.key());
}
const sendResponse = await this.sendApiService.postSendAccess(this.id(), accessRequest);
this.accessGranted.emit({ request: accessRequest, response: sendResponse });
} catch (e) {
if (e instanceof ErrorResponse) {
if (e.statusCode === 401) {
this.sendAuthType.set(AuthType.Password);
} else if (e.statusCode === 404) {
this.unavailable.set(true);
} else {
this.error.set(true);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: e.message,
});
}
} else {
this.error.set(true);
}
}
}
private async attemptV2Access(): Promise<void> {
let sendAccessCreds: SendAccessDomainCredentials | null = null;
if (this.sendAuthType() === AuthType.Email) {
const email = this.sendAccessForm.value.email;
if (email == null) {
return;
}
if (!this.enterOtp()) {
sendAccessCreds = { kind: "email", email };
} else {
const otp = this.sendAccessForm.value.otp as SendOtp;
if (otp == null) {
return;
}
sendAccessCreds = { kind: "email_otp", email, otp };
}
} else if (this.sendAuthType() === AuthType.Password) {
const password = this.sendAccessForm.value.password;
if (password == null) {
return;
}
const passwordHashB64 = await this.getPasswordHashB64(password, this.key());
sendAccessCreds = { kind: "password", passwordHashB64 };
}
const response = !sendAccessCreds
? await firstValueFrom(this.sendTokenService.tryGetSendAccessToken$(this.id()))
: await firstValueFrom(this.sendTokenService.getSendAccessToken$(this.id(), sendAccessCreds));
if (response instanceof SendAccessToken) {
this.expiredAuthAttempts = 0;
this.accessGranted.emit({ accessToken: response });
} else if (response.kind === "expired") {
if (this.expiredAuthAttempts > 2) {
return;
}
this.expiredAuthAttempts++;
await this.attemptV2Access();
} else if (response.kind === "expected_server") {
this.expiredAuthAttempts = 0;
if (emailRequired(response.error)) {
this.sendAuthType.set(AuthType.Email);
this.updatePageTitle();
} else if (emailAndOtpRequired(response.error)) {
this.enterOtp.set(true);
this.updatePageTitle();
} else if (otpInvalid(response.error)) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidVerificationCode"),
});
} else if (passwordHashB64Required(response.error)) {
this.sendAuthType.set(AuthType.Password);
this.updatePageTitle();
} else if (passwordHashB64Invalid(response.error)) {
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: this.i18nService.t("invalidSendPassword"),
});
} else if (sendIdInvalid(response.error)) {
this.unavailable.set(true);
} else {
this.error.set(true);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: response.error.error_description ?? "",
});
}
} else {
this.expiredAuthAttempts = 0;
this.error.set(true);
this.toastService.showToast({
variant: "error",
title: this.i18nService.t("errorOccurred"),
message: response.error,
});
}
}
private async getPasswordHashB64(password: string, key: string) {
const keyArray = Utils.fromUrlB64ToArray(key);
const passwordHash = await this.cryptoFunctionService.pbkdf2(
password,
keyArray,
"sha256",
SEND_KDF_ITERATIONS,
);
return Utils.fromBufferToB64(passwordHash) as SendHashedPasswordB64;
}
private updatePageTitle(): void {
const authType = this.sendAuthType();
if (authType === AuthType.Email) {
if (this.enterOtp()) {
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: { key: "enterTheCodeSentToYourEmail" },
});
} else {
this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({
pageTitle: { key: "verifyYourEmailToViewThisSend" },
});
}
}
}
}