mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
feat(device-approval-persistence): [PM-19380] Device Approval Persistence (#13958)
* feat(device-approval-persistence): [PM-19380] Device Approval Persistence - Added lookup on standard auth requests. * fix(device-approval-persistence): [PM-19380] Device Approval Persistence - Fixed issue with null value trying to be parsed from the fromJSON function. --------- Co-authored-by: Todd Martin <tmartin@bitwarden.com>
This commit is contained in:
committed by
GitHub
parent
b385dd430e
commit
1af8fe2012
@@ -90,7 +90,6 @@ export class LocalBackedSessionStorageService
|
|||||||
this.logService.warning(
|
this.logService.warning(
|
||||||
`Possible unnecessary write to local session storage. Key: ${key}`,
|
`Possible unnecessary write to local session storage. Key: ${key}`,
|
||||||
);
|
);
|
||||||
this.logService.warning(obj as any);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.logService.warning(`Error while comparing values for key: ${key}`);
|
this.logService.warning(`Error while comparing values for key: ${key}`);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ type Deserializer<T> = {
|
|||||||
* @param jsonValue The JSON object representation of your state.
|
* @param jsonValue The JSON object representation of your state.
|
||||||
* @returns The fully typed version of your state.
|
* @returns The fully typed version of your state.
|
||||||
*/
|
*/
|
||||||
readonly deserializer?: (jsonValue: Jsonify<T>) => T;
|
readonly deserializer?: (jsonValue: Jsonify<T>) => T | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type BaseCacheOptions<T> = {
|
type BaseCacheOptions<T> = {
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
# Authentication Flows Documentation
|
||||||
|
|
||||||
|
## Standard Auth Request Flows
|
||||||
|
|
||||||
|
### Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory
|
||||||
|
|
||||||
|
1. Unauthed user clicks "Login with device"
|
||||||
|
2. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||||
|
3. Receives approval from a device with authRequestPublicKey(masterKey)
|
||||||
|
4. Decrypts masterKey
|
||||||
|
5. Decrypts userKey
|
||||||
|
6. Proceeds to vault
|
||||||
|
|
||||||
|
### Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory
|
||||||
|
|
||||||
|
1. Unauthed user clicks "Login with device"
|
||||||
|
2. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||||
|
3. Receives approval from a device with authRequestPublicKey(userKey)
|
||||||
|
4. Decrypts userKey
|
||||||
|
5. Proceeds to vault
|
||||||
|
|
||||||
|
**Note:** This flow is an uncommon scenario and relates to TDE off-boarding. The following describes how a user could
|
||||||
|
get into this flow:
|
||||||
|
|
||||||
|
1. An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT have a masterKey
|
||||||
|
in memory
|
||||||
|
2. The org admin:
|
||||||
|
- Changes the member decryption options from "Trusted devices" to "Master password" AND
|
||||||
|
- Turns off the "Require single sign-on authentication" policy
|
||||||
|
3. On another device, the user clicks "Login with device", which they can do because the org no longer requires SSO
|
||||||
|
4. The user approves from the device they had previously logged into with SSO TD, which does NOT have a masterKey in
|
||||||
|
memory
|
||||||
|
|
||||||
|
### Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory
|
||||||
|
|
||||||
|
1. SSO TD user authenticates via SSO
|
||||||
|
2. Navigates to /login-initiated
|
||||||
|
3. Clicks "Approve from your other device"
|
||||||
|
4. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||||
|
5. Receives approval from device with authRequestPublicKey(masterKey)
|
||||||
|
6. Decrypts masterKey
|
||||||
|
7. Decrypts userKey
|
||||||
|
8. Establishes trust (if required)
|
||||||
|
9. Proceeds to vault
|
||||||
|
|
||||||
|
### Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory
|
||||||
|
|
||||||
|
1. SSO TD user authenticates via SSO
|
||||||
|
2. Navigates to /login-initiated
|
||||||
|
3. Clicks "Approve from your other device"
|
||||||
|
4. Navigates to /login-with-device which creates a StandardAuthRequest
|
||||||
|
5. Receives approval from device with authRequestPublicKey(userKey)
|
||||||
|
6. Decrypts userKey
|
||||||
|
7. Establishes trust (if required)
|
||||||
|
8. Proceeds to vault
|
||||||
|
|
||||||
|
## Admin Auth Request Flow
|
||||||
|
|
||||||
|
### Flow: Authed SSO TD user requests admin approval
|
||||||
|
|
||||||
|
1. SSO TD user authenticates via SSO
|
||||||
|
2. Navigates to /login-initiated
|
||||||
|
3. Clicks "Request admin approval"
|
||||||
|
4. Navigates to /admin-approval-requested which creates an AdminAuthRequest
|
||||||
|
5. Receives approval from device with authRequestPublicKey(userKey)
|
||||||
|
6. Decrypts userKey
|
||||||
|
7. Establishes trust (if required)
|
||||||
|
8. Proceeds to vault
|
||||||
|
|
||||||
|
**Note:** TDE users are required to be enrolled in admin account recovery, which gives the admin access to the user's
|
||||||
|
userKey. This is how admins are able to send over the authRequestPublicKey(userKey) to the user to allow them to unlock.
|
||||||
|
|
||||||
|
## Summary Table
|
||||||
|
|
||||||
|
| Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory\* |
|
||||||
|
| --------------- | ----------- | --------------------------------------------------- | ------------------------- | ------------------------------------------------- |
|
||||||
|
| Standard Flow 1 | unauthed | "Login with device" [/login] | /login-with-device | yes |
|
||||||
|
| Standard Flow 2 | unauthed | "Login with device" [/login] | /login-with-device | no |
|
||||||
|
| Standard Flow 3 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | yes |
|
||||||
|
| Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no |
|
||||||
|
| Admin Flow | authed | "Request admin approval" [/login-initiated] | /admin-approval-requested | NA - admin requests always send encrypted userKey |
|
||||||
|
|
||||||
|
**Note:** The phrase "in memory" here is important. It is possible for a user to have a master password for their
|
||||||
|
account, but not have a masterKey IN MEMORY for a specific device. For example, if a user registers an account with a
|
||||||
|
master password, then joins an SSO TD org, then logs in to a device via SSO and admin auth request, they are now logged
|
||||||
|
into that device but that device does not have masterKey IN MEMORY.
|
||||||
|
|
||||||
|
## State Management
|
||||||
|
|
||||||
|
### View Cache
|
||||||
|
|
||||||
|
The component uses `LoginViaAuthRequestCacheService` to manage persistent state across extension close and reopen.
|
||||||
|
This cache stores:
|
||||||
|
|
||||||
|
- Auth Request ID
|
||||||
|
- Private Key
|
||||||
|
- Access Code
|
||||||
|
|
||||||
|
The cache is used to:
|
||||||
|
|
||||||
|
1. Preserve authentication state during extension close
|
||||||
|
2. Allow resumption of pending auth requests
|
||||||
|
3. Enable processing of approved requests after extension close and reopen.
|
||||||
|
|
||||||
|
### Component State Variables
|
||||||
|
|
||||||
|
Key state variables maintained during the authentication process:
|
||||||
|
|
||||||
|
#### Authentication Keys
|
||||||
|
|
||||||
|
```
|
||||||
|
private authRequestKeyPair: {
|
||||||
|
publicKey: Uint8Array | undefined;
|
||||||
|
privateKey: Uint8Array | undefined;
|
||||||
|
} | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
- Stores the RSA key pair used for secure communication
|
||||||
|
- Generated during auth request initialization
|
||||||
|
- Required for decrypting approved auth responses
|
||||||
|
|
||||||
|
#### Access Code
|
||||||
|
|
||||||
|
```
|
||||||
|
private accessCode: string | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
- 25-character generated password
|
||||||
|
- Used for retrieving auth responses when user is not authenticated
|
||||||
|
- Required for standard auth flows
|
||||||
|
|
||||||
|
#### Authentication Status
|
||||||
|
|
||||||
|
```
|
||||||
|
private authStatus: AuthenticationStatus | undefined
|
||||||
|
```
|
||||||
|
|
||||||
|
- Tracks whether user is authenticated via SSO
|
||||||
|
- Determines available flows and API endpoints
|
||||||
|
- Affects navigation paths (`/login` vs `/login-initiated`)
|
||||||
|
|
||||||
|
#### Flow Control
|
||||||
|
|
||||||
|
```
|
||||||
|
protected flow = Flow.StandardAuthRequest
|
||||||
|
```
|
||||||
|
|
||||||
|
- Determines current authentication flow (Standard vs Admin)
|
||||||
|
- Affects UI rendering and request handling
|
||||||
|
- Set based on route and authentication state
|
||||||
|
|
||||||
|
### State Flow Examples
|
||||||
|
|
||||||
|
#### Standard Auth Request Cache Flow
|
||||||
|
|
||||||
|
1. User initiates login with device
|
||||||
|
2. Component generates auth request and keys
|
||||||
|
3. Cache service stores:
|
||||||
|
```
|
||||||
|
cacheLoginView(
|
||||||
|
authRequestResponse.id,
|
||||||
|
authRequestKeyPair.privateKey,
|
||||||
|
accessCode
|
||||||
|
)
|
||||||
|
```
|
||||||
|
4. On page refresh/revisit:
|
||||||
|
- Component retrieves cached view
|
||||||
|
- Reestablishes connection using cached credentials
|
||||||
|
- Continues monitoring for approval
|
||||||
|
|
||||||
|
#### Admin Auth Request State Flow
|
||||||
|
|
||||||
|
1. User requests admin approval
|
||||||
|
2. Component stores admin request in `AuthRequestService`:
|
||||||
|
```
|
||||||
|
setAdminAuthRequest(
|
||||||
|
new AdminAuthRequestStorable({
|
||||||
|
id: authRequestResponse.id,
|
||||||
|
privateKey: authRequestKeyPair.privateKey
|
||||||
|
}),
|
||||||
|
userId
|
||||||
|
)
|
||||||
|
```
|
||||||
|
3. On subsequent visits:
|
||||||
|
- Component checks for existing admin requests
|
||||||
|
- Either resumes monitoring or starts new request
|
||||||
|
- Clears state after successful approval
|
||||||
|
|
||||||
|
### State Cleanup
|
||||||
|
|
||||||
|
State cleanup occurs in several scenarios:
|
||||||
|
|
||||||
|
- Component destruction (`ngOnDestroy`)
|
||||||
|
- Successful authentication
|
||||||
|
- Request denial or timeout
|
||||||
|
- Manual navigation away
|
||||||
|
|
||||||
|
Key cleanup actions:
|
||||||
|
|
||||||
|
1. Hub connection termination
|
||||||
|
2. Cache clearance
|
||||||
|
3. Admin request state removal
|
||||||
|
4. Key pair disposal
|
||||||
@@ -1,57 +1,65 @@
|
|||||||
<div class="tw-text-center">
|
<ng-container *ngIf="loading">
|
||||||
<ng-container *ngIf="flow === Flow.StandardAuthRequest">
|
<div class="tw-flex tw-items-center tw-justify-center">
|
||||||
<p *ngIf="clientType !== ClientType.Web">
|
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
|
||||||
{{ "notificationSentDevicePart1" | i18n }}
|
</div>
|
||||||
<a
|
</ng-container>
|
||||||
bitLink
|
|
||||||
linkType="primary"
|
|
||||||
class="tw-cursor-pointer"
|
|
||||||
[href]="deviceManagementUrl"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>{{ "notificationSentDeviceAnchor" | i18n }}</a
|
|
||||||
>. {{ "notificationSentDevicePart2" | i18n }}
|
|
||||||
</p>
|
|
||||||
<p *ngIf="clientType === ClientType.Web">
|
|
||||||
{{ "notificationSentDeviceComplete" | i18n }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
|
<ng-container *ngIf="!loading">
|
||||||
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
|
<div class="tw-text-center">
|
||||||
|
<ng-container *ngIf="flow === Flow.StandardAuthRequest">
|
||||||
|
<p *ngIf="clientType !== ClientType.Web">
|
||||||
|
{{ "notificationSentDevicePart1" | i18n }}
|
||||||
|
<a
|
||||||
|
bitLink
|
||||||
|
linkType="primary"
|
||||||
|
class="tw-cursor-pointer"
|
||||||
|
[href]="deviceManagementUrl"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>{{ "notificationSentDeviceAnchor" | i18n }}</a
|
||||||
|
>. {{ "notificationSentDevicePart2" | i18n }}
|
||||||
|
</p>
|
||||||
|
<p *ngIf="clientType === ClientType.Web">
|
||||||
|
{{ "notificationSentDeviceComplete" | i18n }}
|
||||||
|
</p>
|
||||||
|
|
||||||
<button
|
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
|
||||||
*ngIf="showResendNotification"
|
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
|
||||||
type="button"
|
|
||||||
bitButton
|
|
||||||
block
|
|
||||||
buttonType="secondary"
|
|
||||||
class="tw-mt-4"
|
|
||||||
(click)="startStandardAuthRequestLogin(true)"
|
|
||||||
>
|
|
||||||
{{ "resendNotification" | i18n }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div *ngIf="clientType !== ClientType.Browser" class="tw-mt-4">
|
<button
|
||||||
<span>{{ "needAnotherOptionV1" | i18n }}</span
|
*ngIf="showResendNotification"
|
||||||
>
|
type="button"
|
||||||
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
|
bitButton
|
||||||
"viewAllLogInOptions" | i18n
|
block
|
||||||
}}</a>
|
buttonType="secondary"
|
||||||
</div>
|
class="tw-mt-4"
|
||||||
</ng-container>
|
(click)="handleNewStandardAuthRequestLogin()"
|
||||||
|
>
|
||||||
|
{{ "resendNotification" | i18n }}
|
||||||
|
</button>
|
||||||
|
|
||||||
<ng-container *ngIf="flow === Flow.AdminAuthRequest">
|
<div *ngIf="clientType !== ClientType.Browser" class="tw-mt-4">
|
||||||
<p>{{ "youWillBeNotifiedOnceTheRequestIsApproved" | i18n }}</p>
|
<span>{{ "needAnotherOptionV1" | i18n }}</span
|
||||||
|
>
|
||||||
|
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
|
||||||
|
"viewAllLogInOptions" | i18n
|
||||||
|
}}</a>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
|
<ng-container *ngIf="flow === Flow.AdminAuthRequest">
|
||||||
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
|
<p>{{ "youWillBeNotifiedOnceTheRequestIsApproved" | i18n }}</p>
|
||||||
|
|
||||||
<div class="tw-mt-4">
|
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
|
||||||
<span>{{ "troubleLoggingIn" | i18n }}</span
|
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
|
||||||
>
|
|
||||||
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
|
<div class="tw-mt-4">
|
||||||
"viewAllLogInOptions" | i18n
|
<span>{{ "troubleLoggingIn" | i18n }}</span
|
||||||
}}</a>
|
>
|
||||||
</div>
|
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
|
||||||
</ng-container>
|
"viewAllLogInOptions" | i18n
|
||||||
</div>
|
}}</a>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|||||||
@@ -62,12 +62,13 @@ const matchOptions: IsActiveMatchOptions = {
|
|||||||
providers: [{ provide: LoginViaAuthRequestCacheService }],
|
providers: [{ provide: LoginViaAuthRequestCacheService }],
|
||||||
})
|
})
|
||||||
export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||||
private authRequest: AuthRequest | undefined = undefined;
|
|
||||||
private authRequestKeyPair:
|
private authRequestKeyPair:
|
||||||
| { publicKey: Uint8Array | undefined; privateKey: Uint8Array | undefined }
|
| { publicKey: Uint8Array | undefined; privateKey: Uint8Array | undefined }
|
||||||
| undefined = undefined;
|
| undefined = undefined;
|
||||||
|
private accessCode: string | undefined = undefined;
|
||||||
private authStatus: AuthenticationStatus | undefined = undefined;
|
private authStatus: AuthenticationStatus | undefined = undefined;
|
||||||
private showResendNotificationTimeoutSeconds = 12;
|
private showResendNotificationTimeoutSeconds = 12;
|
||||||
|
protected loading = true;
|
||||||
|
|
||||||
protected backToRoute = "/login";
|
protected backToRoute = "/login";
|
||||||
protected clientType: ClientType;
|
protected clientType: ClientType;
|
||||||
@@ -110,13 +111,14 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
this.authRequestService.authRequestPushNotification$
|
this.authRequestService.authRequestPushNotification$
|
||||||
.pipe(takeUntilDestroyed())
|
.pipe(takeUntilDestroyed())
|
||||||
.subscribe((requestId) => {
|
.subscribe((requestId) => {
|
||||||
this.verifyAndHandleApprovedAuthReq(requestId).catch((e: Error) => {
|
this.loading = true;
|
||||||
|
this.handleExistingAuthRequestLogin(requestId).catch((e: Error) => {
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "error",
|
variant: "error",
|
||||||
title: this.i18nService.t("error"),
|
title: this.i18nService.t("error"),
|
||||||
message: e.message,
|
message: e.message,
|
||||||
});
|
});
|
||||||
|
this.loading = false;
|
||||||
this.logService.error("Failed to use approved auth request: " + e.message);
|
this.logService.error("Failed to use approved auth request: " + e.message);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -149,24 +151,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
await this.initStandardAuthRequestFlow();
|
await this.initStandardAuthRequestFlow();
|
||||||
}
|
}
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async initAdminAuthRequestFlow(): Promise<void> {
|
private async initAdminAuthRequestFlow(): Promise<void> {
|
||||||
this.flow = Flow.AdminAuthRequest;
|
this.flow = Flow.AdminAuthRequest;
|
||||||
|
|
||||||
// Get email from state for admin auth requests because it is available and also
|
|
||||||
// prevents it from being lost on refresh as the loginEmailService email does not persist.
|
|
||||||
this.email = await firstValueFrom(
|
|
||||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.email) {
|
|
||||||
await this.handleMissingEmail();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only allow a single admin approval request to be active at a time
|
|
||||||
// so we must check state to see if we have an existing one or not
|
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
this.logService.error(
|
this.logService.error(
|
||||||
@@ -175,12 +165,13 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingAdminAuthRequest = await this.authRequestService.getAdminAuthRequest(userId);
|
// [Admin Request Flow State Management] Check cached auth request
|
||||||
|
const existingAdminAuthRequest = await this.reloadCachedAdminAuthRequest(userId);
|
||||||
|
|
||||||
if (existingAdminAuthRequest) {
|
if (existingAdminAuthRequest) {
|
||||||
await this.handleExistingAdminAuthRequest(existingAdminAuthRequest, userId);
|
await this.handleExistingAdminAuthRequestLogin(existingAdminAuthRequest, userId);
|
||||||
} else {
|
} else {
|
||||||
await this.startAdminAuthRequestLogin();
|
await this.handleNewAdminAuthRequestLogin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,7 +185,24 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.startStandardAuthRequestLogin();
|
// [Standard Flow State Management] Check cached auth request
|
||||||
|
const cachedAuthRequest: LoginViaAuthRequestView | null =
|
||||||
|
this.loginViaAuthRequestCacheService.getCachedLoginViaAuthRequestView();
|
||||||
|
|
||||||
|
if (cachedAuthRequest) {
|
||||||
|
this.logService.info("Found cached auth request.");
|
||||||
|
if (!cachedAuthRequest.id) {
|
||||||
|
this.logService.error(
|
||||||
|
"No id on the cached auth request when in the standard auth request flow.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.reloadCachedStandardAuthRequest(cachedAuthRequest);
|
||||||
|
await this.handleExistingAuthRequestLogin(cachedAuthRequest.id);
|
||||||
|
} else {
|
||||||
|
await this.handleNewStandardAuthRequestLogin();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleMissingEmail(): Promise<void> {
|
private async handleMissingEmail(): Promise<void> {
|
||||||
@@ -212,11 +220,17 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async startAdminAuthRequestLogin(): Promise<void> {
|
private async handleNewAdminAuthRequestLogin(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.buildAuthRequest(AuthRequestType.AdminApproval);
|
if (!this.email) {
|
||||||
|
this.logService.error("No email when starting admin auth request login.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.authRequest) {
|
// At this point we know there is no
|
||||||
|
const authRequest = await this.buildAuthRequest(this.email, AuthRequestType.AdminApproval);
|
||||||
|
|
||||||
|
if (!authRequest) {
|
||||||
this.logService.error("Auth request failed to build.");
|
this.logService.error("Auth request failed to build.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -226,9 +240,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authRequestResponse = await this.authRequestApiService.postAdminAuthRequest(
|
const authRequestResponse =
|
||||||
this.authRequest as AuthRequest,
|
await this.authRequestApiService.postAdminAuthRequest(authRequest);
|
||||||
);
|
|
||||||
const adminAuthReqStorable = new AdminAuthRequestStorable({
|
const adminAuthReqStorable = new AdminAuthRequestStorable({
|
||||||
id: authRequestResponse.id,
|
id: authRequestResponse.id,
|
||||||
privateKey: this.authRequestKeyPair.privateKey,
|
privateKey: this.authRequestKeyPair.privateKey,
|
||||||
@@ -253,104 +267,154 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async startStandardAuthRequestLogin(
|
/**
|
||||||
clearCachedRequest: boolean = false,
|
* We only allow a single admin approval request to be active at a time
|
||||||
|
* so we can check to see if it's stored in state with the state service
|
||||||
|
* provider.
|
||||||
|
* @param userId
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async reloadCachedAdminAuthRequest(
|
||||||
|
userId: UserId,
|
||||||
|
): Promise<AdminAuthRequestStorable | null> {
|
||||||
|
// Get email from state for admin auth requests because it is available and also
|
||||||
|
// prevents it from being lost on refresh as the loginEmailService email does not persist.
|
||||||
|
this.email = await firstValueFrom(
|
||||||
|
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!this.email) {
|
||||||
|
await this.handleMissingEmail();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.authRequestService.getAdminAuthRequest(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores a cached authentication request into the component's state.
|
||||||
|
*
|
||||||
|
* This function checks for the presence of a cached authentication request and,
|
||||||
|
* if available, updates the component's state with the necessary details to
|
||||||
|
* continue processing the request. It ensures that the user's email and the
|
||||||
|
* private key from the cached request are available.
|
||||||
|
*
|
||||||
|
* The private key is converted from Base64 to an ArrayBuffer, and a fingerprint
|
||||||
|
* phrase is derived to verify the request's integrity. The function then sets
|
||||||
|
* the authentication request key pair in the component's state, preparing it
|
||||||
|
* to handle any responses or approvals.
|
||||||
|
*
|
||||||
|
* @param cachedAuthRequest The request to load into the component state
|
||||||
|
* @returns Promise to await for completion
|
||||||
|
*/
|
||||||
|
protected async reloadCachedStandardAuthRequest(
|
||||||
|
cachedAuthRequest: LoginViaAuthRequestView,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
if (cachedAuthRequest) {
|
||||||
|
if (!this.email) {
|
||||||
|
this.logService.error(
|
||||||
|
"Email not defined when trying to reload cached standard auth request.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cachedAuthRequest.privateKey) {
|
||||||
|
this.logService.error(
|
||||||
|
"No private key on the cached auth request when trying to reload cached standard auth request.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cachedAuthRequest.accessCode) {
|
||||||
|
this.logService.error(
|
||||||
|
"No access code on the cached auth request when trying to reload cached standard auth request.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateKey = Utils.fromB64ToArray(cachedAuthRequest.privateKey);
|
||||||
|
|
||||||
|
// Re-derive the user's fingerprint phrase
|
||||||
|
// It is important to not use the server's public key here as it could have been compromised via MITM
|
||||||
|
const derivedPublicKeyArrayBuffer =
|
||||||
|
await this.cryptoFunctionService.rsaExtractPublicKey(privateKey);
|
||||||
|
|
||||||
|
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||||
|
this.email,
|
||||||
|
derivedPublicKeyArrayBuffer,
|
||||||
|
);
|
||||||
|
|
||||||
|
// We don't need the public key for handling the authentication request because
|
||||||
|
// the handleExistingAuthRequestLogin function will receive the public key back
|
||||||
|
// from the looked up auth request, and all we need is to make sure that
|
||||||
|
// we can use the cached private key that is associated with it.
|
||||||
|
this.authRequestKeyPair = {
|
||||||
|
privateKey: privateKey,
|
||||||
|
publicKey: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.accessCode = cachedAuthRequest.accessCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleNewStandardAuthRequestLogin(): Promise<void> {
|
||||||
this.showResendNotification = false;
|
this.showResendNotification = false;
|
||||||
|
|
||||||
if (await this.configService.getFeatureFlag(FeatureFlag.PM9112_DeviceApprovalPersistence)) {
|
try {
|
||||||
// Used for manually refreshing the auth request when clicking the resend auth request
|
if (!this.email) {
|
||||||
// on the ui.
|
this.logService.error("Email not defined when starting standard auth request login.");
|
||||||
if (clearCachedRequest) {
|
return;
|
||||||
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const authRequest = await this.buildAuthRequest(
|
||||||
const loginAuthRequestView: LoginViaAuthRequestView | null =
|
this.email,
|
||||||
this.loginViaAuthRequestCacheService.getCachedLoginViaAuthRequestView();
|
AuthRequestType.AuthenticateAndUnlock,
|
||||||
|
);
|
||||||
|
|
||||||
if (!loginAuthRequestView) {
|
// I tried several ways to get the IDE/linter to play nice with checking for null values
|
||||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
// in less code / more efficiently, but it struggles to identify code paths that
|
||||||
|
// are more complicated than this.
|
||||||
// I tried several ways to get the IDE/linter to play nice with checking for null values
|
if (!authRequest) {
|
||||||
// in less code / more efficiently, but it struggles to identify code paths that
|
this.logService.error("AuthRequest failed to initialize from buildAuthRequest.");
|
||||||
// are more complicated than this.
|
return;
|
||||||
if (!this.authRequest) {
|
|
||||||
this.logService.error("AuthRequest failed to initialize from buildAuthRequest.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.fingerprintPhrase) {
|
|
||||||
this.logService.error("FingerprintPhrase failed to initialize from buildAuthRequest.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.authRequestKeyPair) {
|
|
||||||
this.logService.error("KeyPair failed to initialize from buildAuthRequest.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const authRequestResponse: AuthRequestResponse =
|
|
||||||
await this.authRequestApiService.postAuthRequest(this.authRequest);
|
|
||||||
|
|
||||||
this.loginViaAuthRequestCacheService.cacheLoginView(
|
|
||||||
this.authRequest,
|
|
||||||
authRequestResponse,
|
|
||||||
this.fingerprintPhrase,
|
|
||||||
this.authRequestKeyPair,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (authRequestResponse.id) {
|
|
||||||
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Grab the cached information and store it back in component state.
|
|
||||||
// We don't need the public key for handling the authentication request because
|
|
||||||
// the verifyAndHandleApprovedAuthReq function will receive the public key back
|
|
||||||
// from the looked up auth request and all we need is to make sure that
|
|
||||||
// we can use the cached private key that is associated with it.
|
|
||||||
this.authRequest = loginAuthRequestView.authRequest;
|
|
||||||
this.fingerprintPhrase = loginAuthRequestView.fingerprintPhrase;
|
|
||||||
this.authRequestKeyPair = {
|
|
||||||
privateKey: loginAuthRequestView.privateKey
|
|
||||||
? Utils.fromB64ToArray(loginAuthRequestView.privateKey)
|
|
||||||
: undefined,
|
|
||||||
publicKey: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!loginAuthRequestView.authRequestResponse) {
|
|
||||||
this.logService.error("No cached auth request response.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loginAuthRequestView.authRequestResponse.id) {
|
|
||||||
await this.anonymousHubService.createHubConnection(
|
|
||||||
loginAuthRequestView.authRequestResponse.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await this.buildAuthRequest(AuthRequestType.AuthenticateAndUnlock);
|
|
||||||
|
|
||||||
if (!this.authRequest) {
|
if (!this.fingerprintPhrase) {
|
||||||
this.logService.error("No auth request found.");
|
this.logService.error("FingerprintPhrase failed to initialize from buildAuthRequest.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.authRequestKeyPair) {
|
||||||
|
this.logService.error("KeyPair failed to initialize from buildAuthRequest.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authRequestResponse: AuthRequestResponse =
|
||||||
|
await this.authRequestApiService.postAuthRequest(authRequest);
|
||||||
|
|
||||||
|
if (await this.configService.getFeatureFlag(FeatureFlag.PM9112_DeviceApprovalPersistence)) {
|
||||||
|
if (!this.authRequestKeyPair.privateKey) {
|
||||||
|
this.logService.error("No private key when trying to cache the login view.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const authRequestResponse = await this.authRequestApiService.postAuthRequest(
|
if (!this.accessCode) {
|
||||||
this.authRequest,
|
this.logService.error("No access code when trying to cache the login view.");
|
||||||
);
|
return;
|
||||||
|
|
||||||
if (authRequestResponse.id) {
|
|
||||||
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
this.loginViaAuthRequestCacheService.cacheLoginView(
|
||||||
|
authRequestResponse.id,
|
||||||
|
this.authRequestKeyPair.privateKey,
|
||||||
|
this.accessCode,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (authRequestResponse.id) {
|
||||||
|
await this.anonymousHubService.createHubConnection(authRequestResponse.id);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -358,7 +422,10 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
}, this.showResendNotificationTimeoutSeconds * 1000);
|
}, this.showResendNotificationTimeoutSeconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async buildAuthRequest(authRequestType: AuthRequestType): Promise<void> {
|
private async buildAuthRequest(
|
||||||
|
email: string,
|
||||||
|
authRequestType: AuthRequestType,
|
||||||
|
): Promise<AuthRequest> {
|
||||||
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
const authRequestKeyPairArray = await this.cryptoFunctionService.rsaGenerateKeyPair(2048);
|
||||||
|
|
||||||
this.authRequestKeyPair = {
|
this.authRequestKeyPair = {
|
||||||
@@ -369,36 +436,27 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
const deviceIdentifier = await this.appIdService.getAppId();
|
const deviceIdentifier = await this.appIdService.getAppId();
|
||||||
|
|
||||||
if (!this.authRequestKeyPair.publicKey) {
|
if (!this.authRequestKeyPair.publicKey) {
|
||||||
this.logService.error("AuthRequest public key not set to value in building auth request.");
|
const errorMessage = "No public key when building an auth request.";
|
||||||
return;
|
this.logService.error(errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||||
const accessCode = await this.passwordGenerationService.generatePassword({
|
email,
|
||||||
|
this.authRequestKeyPair.publicKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.accessCode = await this.passwordGenerationService.generatePassword({
|
||||||
type: "password",
|
type: "password",
|
||||||
length: 25,
|
length: 25,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!this.email) {
|
const b64PublicKey = Utils.fromBufferToB64(this.authRequestKeyPair.publicKey);
|
||||||
this.logService.error("Email not defined when building auth request.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
return new AuthRequest(email, deviceIdentifier, b64PublicKey, authRequestType, this.accessCode);
|
||||||
this.email,
|
|
||||||
this.authRequestKeyPair.publicKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.authRequest = new AuthRequest(
|
|
||||||
this.email,
|
|
||||||
deviceIdentifier,
|
|
||||||
publicKey,
|
|
||||||
authRequestType,
|
|
||||||
accessCode,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleExistingAdminAuthRequest(
|
private async handleExistingAdminAuthRequestLogin(
|
||||||
adminAuthRequestStorable: AdminAuthRequestStorable,
|
adminAuthRequestStorable: AdminAuthRequestStorable,
|
||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@@ -414,7 +472,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
return await this.clearExistingAdminAuthRequestAndStartNewRequest(userId);
|
||||||
}
|
}
|
||||||
this.logService.error(error);
|
this.logService.error(error);
|
||||||
return;
|
return;
|
||||||
@@ -422,28 +480,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
// Request doesn't exist anymore
|
// Request doesn't exist anymore
|
||||||
if (!adminAuthRequestResponse) {
|
if (!adminAuthRequestResponse) {
|
||||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
return await this.clearExistingAdminAuthRequestAndStartNewRequest(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-derive the user's fingerprint phrase
|
|
||||||
// It is important to not use the server's public key here as it could have been compromised via MITM
|
|
||||||
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
|
|
||||||
adminAuthRequestStorable.privateKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!this.email) {
|
|
||||||
this.logService.error("Email not defined when handling an existing an admin auth request.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
|
||||||
this.email,
|
|
||||||
derivedPublicKeyArrayBuffer,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Request denied
|
// Request denied
|
||||||
if (adminAuthRequestResponse.isAnswered && !adminAuthRequestResponse.requestApproved) {
|
if (adminAuthRequestResponse.isAnswered && !adminAuthRequestResponse.requestApproved) {
|
||||||
return await this.handleExistingAdminAuthReqDeletedOrDenied(userId);
|
return await this.clearExistingAdminAuthRequestAndStartNewRequest(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request approved
|
// Request approved
|
||||||
@@ -455,6 +497,22 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.email) {
|
||||||
|
this.logService.error("Email not defined when handling an existing an admin auth request.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-derive the user's fingerprint phrase
|
||||||
|
// It is important to not use the server's public key here as it could have been compromised via MITM
|
||||||
|
const derivedPublicKeyArrayBuffer = await this.cryptoFunctionService.rsaExtractPublicKey(
|
||||||
|
adminAuthRequestStorable.privateKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||||
|
this.email,
|
||||||
|
derivedPublicKeyArrayBuffer,
|
||||||
|
);
|
||||||
|
|
||||||
// Request still pending response from admin set keypair and create hub connection
|
// Request still pending response from admin set keypair and create hub connection
|
||||||
// so that any approvals will be received via push notification
|
// so that any approvals will be received via push notification
|
||||||
this.authRequestKeyPair = {
|
this.authRequestKeyPair = {
|
||||||
@@ -464,117 +522,99 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id);
|
await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async verifyAndHandleApprovedAuthReq(requestId: string): Promise<void> {
|
/**
|
||||||
/**
|
* This is used for trying to get the auth request back out of state.
|
||||||
* ***********************************
|
* @param requestId
|
||||||
* Standard Auth Request Flows
|
* @private
|
||||||
* ***********************************
|
*/
|
||||||
*
|
private async retrieveAuthRequest(requestId: string): Promise<AuthRequestResponse> {
|
||||||
* Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory.
|
let authRequestResponse: AuthRequestResponse | undefined = undefined;
|
||||||
*
|
|
||||||
* Unauthed user clicks "Login with device" > navigates to /login-with-device which creates a StandardAuthRequest
|
|
||||||
* > receives approval from a device with authRequestPublicKey(masterKey) > decrypts masterKey > decrypts userKey > proceed to vault
|
|
||||||
*
|
|
||||||
* Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory.
|
|
||||||
*
|
|
||||||
* Unauthed user clicks "Login with device" > navigates to /login-with-device which creates a StandardAuthRequest
|
|
||||||
* > receives approval from a device with authRequestPublicKey(userKey) > decrypts userKey > proceeds to vault
|
|
||||||
*
|
|
||||||
* Note: this flow is an uncommon scenario and relates to TDE off-boarding. The following describes how a user could get into this flow:
|
|
||||||
* 1) An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT have a masterKey in memory.
|
|
||||||
* 2) The org admin...
|
|
||||||
* (2a) Changes the member decryption options from "Trusted devices" to "Master password" AND
|
|
||||||
* (2b) Turns off the "Require single sign-on authentication" policy
|
|
||||||
* 3) On another device, the user clicks "Login with device", which they can do because the org no longer requires SSO.
|
|
||||||
* 4) The user approves from the device they had previously logged into with SSO TD, which does NOT have a masterKey in memory (see step 1 above).
|
|
||||||
*
|
|
||||||
* Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory.
|
|
||||||
*
|
|
||||||
* SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Approve from your other device"
|
|
||||||
* > navigates to /login-with-device which creates a StandardAuthRequest > receives approval from device with authRequestPublicKey(masterKey)
|
|
||||||
* > decrypts masterKey > decrypts userKey > establishes trust (if required) > proceeds to vault
|
|
||||||
*
|
|
||||||
* Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory.
|
|
||||||
*
|
|
||||||
* SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Approve from your other device"
|
|
||||||
* > navigates to /login-with-device which creates a StandardAuthRequest > receives approval from device with authRequestPublicKey(userKey)
|
|
||||||
* > decrypts userKey > establishes trust (if required) > proceeds to vault
|
|
||||||
*
|
|
||||||
* ***********************************
|
|
||||||
* Admin Auth Request Flow
|
|
||||||
* ***********************************
|
|
||||||
*
|
|
||||||
* Flow: Authed SSO TD user requests admin approval.
|
|
||||||
*
|
|
||||||
* SSO TD user authenticates via SSO > navigates to /login-initiated > clicks "Request admin approval"
|
|
||||||
* > navigates to /admin-approval-requested which creates an AdminAuthRequest > receives approval from device with authRequestPublicKey(userKey)
|
|
||||||
* > decrypts userKey > establishes trust (if required) > proceeds to vault
|
|
||||||
*
|
|
||||||
* Note: TDE users are required to be enrolled in admin password reset, which gives the admin access to the user's userKey.
|
|
||||||
* This is how admins are able to send over the authRequestPublicKey(userKey) to the user to allow them to unlock.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Summary Table
|
|
||||||
* |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
* | Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory (see note 1) |
|
|
||||||
* |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
* | Standard Flow 1 | unauthed | "Login with device" [/login] | /login-with-device | yes |
|
|
||||||
* | Standard Flow 2 | unauthed | "Login with device" [/login] | /login-with-device | no |
|
|
||||||
* | Standard Flow 3 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | yes |
|
|
||||||
* | Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no |
|
|
||||||
* | Admin Flow | authed | "Request admin approval" [/login-initiated] | /admin-approval-requested | NA - admin requests always send encrypted userKey |
|
|
||||||
* |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
||||||
* * Note 1: The phrase "in memory" here is important. It is possible for a user to have a master password for their account, but not have a masterKey IN MEMORY for
|
|
||||||
* a specific device. For example, if a user registers an account with a master password, then joins an SSO TD org, then logs in to a device via SSO and
|
|
||||||
* admin auth request, they are now logged into that device but that device does not have masterKey IN MEMORY.
|
|
||||||
*/
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// There are two cases here, the first being
|
||||||
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
|
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
|
||||||
|
|
||||||
|
// Get the response based on whether we've authenticated or not. We need to call a different API method
|
||||||
|
// based on whether we have a token or need to use the accessCode.
|
||||||
if (userHasAuthenticatedViaSSO) {
|
if (userHasAuthenticatedViaSSO) {
|
||||||
// Get the auth request from the server
|
authRequestResponse = await this.authRequestApiService.getAuthRequest(requestId);
|
||||||
// User is authenticated, therefore the endpoint does not require an access code.
|
|
||||||
const authRequestResponse = await this.authRequestApiService.getAuthRequest(requestId);
|
|
||||||
|
|
||||||
if (authRequestResponse.requestApproved) {
|
|
||||||
// Handles Standard Flows 3-4 and Admin Flow
|
|
||||||
await this.handleAuthenticatedFlows(authRequestResponse);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (!this.authRequest) {
|
if (!this.accessCode) {
|
||||||
this.logService.error("No auth request defined when handling approved auth request.");
|
const errorMessage = "No access code available when handling approved auth request.";
|
||||||
return;
|
this.logService.error(errorMessage);
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
authRequestResponse = await this.authRequestApiService.getAuthResponse(
|
||||||
// Get the auth request from the server
|
|
||||||
// User is unauthenticated, therefore the endpoint requires an access code for user verification.
|
|
||||||
const authRequestResponse = await this.authRequestApiService.getAuthResponse(
|
|
||||||
requestId,
|
requestId,
|
||||||
this.authRequest.accessCode,
|
this.accessCode,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If the request no longer exists, we treat it as if it's been answered (and denied).
|
||||||
|
if (error instanceof ErrorResponse && error.statusCode === HttpStatusCode.NotFound) {
|
||||||
|
authRequestResponse = undefined;
|
||||||
|
} else {
|
||||||
|
this.logService.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (authRequestResponse.requestApproved) {
|
if (authRequestResponse === undefined) {
|
||||||
// Handles Standard Flows 1-2
|
throw new Error("Auth request response not generated");
|
||||||
await this.handleUnauthenticatedFlows(authRequestResponse, requestId);
|
}
|
||||||
|
|
||||||
|
return authRequestResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the Auth Request has been approved, deleted or denied, and handles
|
||||||
|
* the response accordingly.
|
||||||
|
* @param requestId The ID of the Auth Request to process
|
||||||
|
* @returns A boolean indicating whether the Auth Request was successfully processed
|
||||||
|
*/
|
||||||
|
private async handleExistingAuthRequestLogin(requestId: string): Promise<void> {
|
||||||
|
this.showResendNotification = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const authRequestResponse = await this.retrieveAuthRequest(requestId);
|
||||||
|
|
||||||
|
// Request doesn't exist anymore, so we'll clear the cache and start a new request.
|
||||||
|
if (!authRequestResponse) {
|
||||||
|
return await this.clearExistingStandardAuthRequestAndStartNewRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request denied, so we'll clear the cache and start a new request.
|
||||||
|
if (authRequestResponse.isAnswered && !authRequestResponse.requestApproved) {
|
||||||
|
return await this.clearExistingStandardAuthRequestAndStartNewRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request approved, so we'll log the user in.
|
||||||
|
if (authRequestResponse.requestApproved) {
|
||||||
|
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
|
||||||
|
if (userHasAuthenticatedViaSSO) {
|
||||||
|
// [Standard Flow 3-4] Handle authenticated SSO TD user flows
|
||||||
|
return await this.handleAuthenticatedFlows(authRequestResponse);
|
||||||
|
} else {
|
||||||
|
// [Standard Flow 1-2] Handle unauthenticated user flows
|
||||||
|
return await this.handleUnauthenticatedFlows(authRequestResponse, requestId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// At this point, we know that the request is still pending, so we'll start a hub connection to listen for a response.
|
||||||
|
await this.anonymousHubService.createHubConnection(requestId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ErrorResponse) {
|
if (error instanceof ErrorResponse) {
|
||||||
await this.router.navigate([this.backToRoute]);
|
await this.router.navigate([this.backToRoute]);
|
||||||
this.validationService.showError(error);
|
this.validationService.showError(error);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logService.error(error);
|
this.logService.error(error);
|
||||||
} finally {
|
|
||||||
// Manually clean out the cache to make sure sensitive
|
|
||||||
// data does not persist longer than it needs to.
|
|
||||||
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.showResendNotification = true;
|
||||||
|
}, this.showResendNotificationTimeoutSeconds * 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) {
|
private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) {
|
||||||
|
// [Standard Flow 3-4] Handle authenticated SSO TD user flows
|
||||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
this.logService.error(
|
this.logService.error(
|
||||||
@@ -599,6 +639,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
authRequestResponse: AuthRequestResponse,
|
authRequestResponse: AuthRequestResponse,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
) {
|
) {
|
||||||
|
// [Standard Flow 1-2] Handle unauthenticated user flows
|
||||||
const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials(
|
const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials(
|
||||||
requestId,
|
requestId,
|
||||||
authRequestResponse,
|
authRequestResponse,
|
||||||
@@ -609,6 +650,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear the cached auth request from state since we're using it to log in.
|
||||||
|
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
||||||
|
|
||||||
// Note: keys are set by AuthRequestLoginStrategy success handling
|
// Note: keys are set by AuthRequestLoginStrategy success handling
|
||||||
const authResult = await this.loginStrategyService.logIn(authRequestLoginCredentials);
|
const authResult = await this.loginStrategyService.logIn(authRequestLoginCredentials);
|
||||||
|
|
||||||
@@ -621,21 +665,20 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
userId: UserId,
|
userId: UserId,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
/**
|
/**
|
||||||
* See verifyAndHandleApprovedAuthReq() for flow details.
|
* [Flow Type Detection]
|
||||||
*
|
|
||||||
* We determine the type of `key` based on the presence or absence of `masterPasswordHash`:
|
* We determine the type of `key` based on the presence or absence of `masterPasswordHash`:
|
||||||
* - If `masterPasswordHash` has a value, we receive the `key` as an authRequestPublicKey(masterKey) [plus we have authRequestPublicKey(masterPasswordHash)]
|
* - If `masterPasswordHash` exists: Standard Flow 1 or 3 (device has masterKey)
|
||||||
* - If `masterPasswordHash` does not have a value, we receive the `key` as an authRequestPublicKey(userKey)
|
* - If no `masterPasswordHash`: Standard Flow 2, 4, or Admin Flow (device sends userKey)
|
||||||
*/
|
*/
|
||||||
if (authRequestResponse.masterPasswordHash) {
|
if (authRequestResponse.masterPasswordHash) {
|
||||||
// ...in Standard Auth Request Flow 3
|
// [Standard Flow 1 or 3] Device has masterKey
|
||||||
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||||
authRequestResponse,
|
authRequestResponse,
|
||||||
privateKey,
|
privateKey,
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// ...in Standard Auth Request Flow 4 or Admin Auth Request Flow
|
// [Standard Flow 2, 4, or Admin Flow] Device sends userKey
|
||||||
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
|
await this.authRequestService.setUserKeyAfterDecryptingSharedUserKey(
|
||||||
authRequestResponse,
|
authRequestResponse,
|
||||||
privateKey,
|
privateKey,
|
||||||
@@ -643,15 +686,20 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Admin Flow Cleanup] Clear one-time use admin auth request
|
||||||
// clear the admin auth request from state so it cannot be used again (it's a one time use)
|
// clear the admin auth request from state so it cannot be used again (it's a one time use)
|
||||||
// TODO: this should eventually be enforced via deleting this on the server once it is used
|
// TODO: this should eventually be enforced via deleting this on the server once it is used
|
||||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||||
|
|
||||||
|
// [Standard Flow Cleanup] Clear the cached auth request from state
|
||||||
|
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
||||||
|
|
||||||
this.toastService.showToast({
|
this.toastService.showToast({
|
||||||
variant: "success",
|
variant: "success",
|
||||||
message: this.i18nService.t("loginApproved"),
|
message: this.i18nService.t("loginApproved"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// [Device Trust] Establish trust if required
|
||||||
// Now that we have a decrypted user key in memory, we can check if we
|
// Now that we have a decrypted user key in memory, we can check if we
|
||||||
// need to establish trust on the current device
|
// need to establish trust on the current device
|
||||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||||
@@ -686,9 +734,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.authRequest) {
|
if (!this.accessCode) {
|
||||||
this.logService.error(
|
this.logService.error(
|
||||||
"AuthRequest not defined when building auth request login credentials.",
|
"Access code not defined when building auth request login credentials.",
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -711,7 +759,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
return new AuthRequestLoginCredentials(
|
return new AuthRequestLoginCredentials(
|
||||||
this.email,
|
this.email,
|
||||||
this.authRequest.accessCode,
|
this.accessCode,
|
||||||
requestId,
|
requestId,
|
||||||
null, // no userKey
|
null, // no userKey
|
||||||
masterKey,
|
masterKey,
|
||||||
@@ -725,7 +773,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
return new AuthRequestLoginCredentials(
|
return new AuthRequestLoginCredentials(
|
||||||
this.email,
|
this.email,
|
||||||
this.authRequest.accessCode,
|
this.accessCode,
|
||||||
requestId,
|
requestId,
|
||||||
userKey,
|
userKey,
|
||||||
null, // no masterKey
|
null, // no masterKey
|
||||||
@@ -734,12 +782,20 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleExistingAdminAuthReqDeletedOrDenied(userId: UserId) {
|
private async clearExistingAdminAuthRequestAndStartNewRequest(userId: UserId) {
|
||||||
// clear the admin auth request from state
|
// clear the admin auth request from state
|
||||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||||
|
|
||||||
// start new auth request
|
// start new auth request
|
||||||
await this.startAdminAuthRequestLogin();
|
await this.handleNewAdminAuthRequestLogin();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async clearExistingStandardAuthRequestAndStartNewRequest(): Promise<void> {
|
||||||
|
// clear the auth request from state
|
||||||
|
this.loginViaAuthRequestCacheService.clearCacheLoginView();
|
||||||
|
|
||||||
|
// start new auth request
|
||||||
|
await this.handleNewStandardAuthRequestLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handlePostLoginNavigation(loginResponse: AuthResult) {
|
private async handlePostLoginNavigation(loginResponse: AuthResult) {
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ import { signal } from "@angular/core";
|
|||||||
import { TestBed } from "@angular/core/testing";
|
import { TestBed } from "@angular/core/testing";
|
||||||
|
|
||||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||||
import { AuthRequestType } from "@bitwarden/common/auth/enums/auth-request-type";
|
|
||||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
|
||||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
|
|
||||||
import { LoginViaAuthRequestCacheService } from "./default-login-via-auth-request-cache.service";
|
import { LoginViaAuthRequestCacheService } from "./default-login-via-auth-request-cache.service";
|
||||||
|
|
||||||
@@ -39,12 +37,12 @@ describe("LoginViaAuthRequestCache", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => {
|
it("`getCachedLoginViaAuthRequestView` returns the cached data", async () => {
|
||||||
cacheSignal.set({ ...buildAuthenticMockAuthView() });
|
cacheSignal.set({ ...buildMockState() });
|
||||||
service = testBed.inject(LoginViaAuthRequestCacheService);
|
service = testBed.inject(LoginViaAuthRequestCacheService);
|
||||||
await service.init();
|
await service.init();
|
||||||
|
|
||||||
expect(service.getCachedLoginViaAuthRequestView()).toEqual({
|
expect(service.getCachedLoginViaAuthRequestView()).toEqual({
|
||||||
...buildAuthenticMockAuthView(),
|
...buildMockState(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -54,20 +52,19 @@ describe("LoginViaAuthRequestCache", () => {
|
|||||||
|
|
||||||
const parameters = buildAuthenticMockAuthView();
|
const parameters = buildAuthenticMockAuthView();
|
||||||
|
|
||||||
service.cacheLoginView(
|
service.cacheLoginView(parameters.id, parameters.privateKey, parameters.accessCode);
|
||||||
parameters.authRequest,
|
|
||||||
parameters.authRequestResponse,
|
|
||||||
parameters.fingerprintPhrase,
|
|
||||||
{ publicKey: new Uint8Array(), privateKey: new Uint8Array() },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(cacheSignal.set).toHaveBeenCalledWith(parameters);
|
expect(cacheSignal.set).toHaveBeenCalledWith({
|
||||||
|
id: parameters.id,
|
||||||
|
privateKey: Utils.fromBufferToB64(parameters.privateKey),
|
||||||
|
accessCode: parameters.accessCode,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("feature disabled", () => {
|
describe("feature disabled", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
cacheSignal.set({ ...buildAuthenticMockAuthView() } as LoginViaAuthRequestView);
|
cacheSignal.set({ ...buildMockState() } as LoginViaAuthRequestView);
|
||||||
getFeatureFlag.mockResolvedValue(false);
|
getFeatureFlag.mockResolvedValue(false);
|
||||||
cacheSetMock.mockClear();
|
cacheSetMock.mockClear();
|
||||||
|
|
||||||
@@ -82,12 +79,7 @@ describe("LoginViaAuthRequestCache", () => {
|
|||||||
it("does not update the signal value", () => {
|
it("does not update the signal value", () => {
|
||||||
const params = buildAuthenticMockAuthView();
|
const params = buildAuthenticMockAuthView();
|
||||||
|
|
||||||
service.cacheLoginView(
|
service.cacheLoginView(params.id, params.privateKey, params.accessCode);
|
||||||
params.authRequest,
|
|
||||||
params.authRequestResponse,
|
|
||||||
params.fingerprintPhrase,
|
|
||||||
{ publicKey: new Uint8Array(), privateKey: new Uint8Array() },
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(cacheSignal.set).not.toHaveBeenCalled();
|
expect(cacheSignal.set).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -95,17 +87,17 @@ describe("LoginViaAuthRequestCache", () => {
|
|||||||
|
|
||||||
const buildAuthenticMockAuthView = () => {
|
const buildAuthenticMockAuthView = () => {
|
||||||
return {
|
return {
|
||||||
fingerprintPhrase: "",
|
id: "testId",
|
||||||
privateKey: "",
|
privateKey: new Uint8Array(),
|
||||||
publicKey: "",
|
accessCode: "testAccessCode",
|
||||||
authRequest: new AuthRequest(
|
};
|
||||||
"test@gmail.com",
|
};
|
||||||
"deviceIdentifier",
|
|
||||||
"publicKey",
|
const buildMockState = () => {
|
||||||
AuthRequestType.Unlock,
|
return {
|
||||||
"accessCode",
|
id: "testId",
|
||||||
),
|
privateKey: Utils.fromBufferToB64(new Uint8Array()),
|
||||||
authRequestResponse: new AuthRequestResponse({}),
|
accessCode: "testAccessCode",
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { inject, Injectable, WritableSignal } from "@angular/core";
|
import { inject, Injectable, WritableSignal } from "@angular/core";
|
||||||
|
|
||||||
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
import { ViewCacheService } from "@bitwarden/angular/platform/abstractions/view-cache.service";
|
||||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
|
||||||
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
import { LoginViaAuthRequestView } from "@bitwarden/common/auth/models/view/login-via-auth-request.view";
|
||||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
@@ -45,12 +43,7 @@ export class LoginViaAuthRequestCacheService {
|
|||||||
/**
|
/**
|
||||||
* Update the cache with the new LoginView.
|
* Update the cache with the new LoginView.
|
||||||
*/
|
*/
|
||||||
cacheLoginView(
|
cacheLoginView(id: string, privateKey: Uint8Array, accessCode: string): void {
|
||||||
authRequest: AuthRequest,
|
|
||||||
authRequestResponse: AuthRequestResponse,
|
|
||||||
fingerprintPhrase: string,
|
|
||||||
keys: { privateKey: Uint8Array | undefined; publicKey: Uint8Array | undefined },
|
|
||||||
): void {
|
|
||||||
if (!this.featureEnabled) {
|
if (!this.featureEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -59,11 +52,9 @@ export class LoginViaAuthRequestCacheService {
|
|||||||
// data can be properly formed when json-ified. If not done, they are not stored properly and
|
// data can be properly formed when json-ified. If not done, they are not stored properly and
|
||||||
// will not be parsable by the cryptography library after coming out of storage.
|
// will not be parsable by the cryptography library after coming out of storage.
|
||||||
this.defaultLoginViaAuthRequestCache.set({
|
this.defaultLoginViaAuthRequestCache.set({
|
||||||
authRequest,
|
id: id,
|
||||||
authRequestResponse,
|
privateKey: Utils.fromBufferToB64(privateKey.buffer),
|
||||||
fingerprintPhrase,
|
accessCode: accessCode,
|
||||||
privateKey: keys.privateKey ? Utils.fromBufferToB64(keys.privateKey.buffer) : undefined,
|
|
||||||
publicKey: keys.publicKey ? Utils.fromBufferToB64(keys.publicKey.buffer) : undefined,
|
|
||||||
} as LoginViaAuthRequestView);
|
} as LoginViaAuthRequestView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import { Jsonify } from "type-fest";
|
import { Jsonify } from "type-fest";
|
||||||
|
|
||||||
import { AuthRequest } from "@bitwarden/common/auth/models/request/auth.request";
|
|
||||||
import { AuthRequestResponse } from "@bitwarden/common/auth/models/response/auth-request.response";
|
|
||||||
import { View } from "@bitwarden/common/models/view/view";
|
import { View } from "@bitwarden/common/models/view/view";
|
||||||
|
|
||||||
export class LoginViaAuthRequestView implements View {
|
export class LoginViaAuthRequestView implements View {
|
||||||
authRequest: AuthRequest | undefined = undefined;
|
id: string | undefined = undefined;
|
||||||
authRequestResponse: AuthRequestResponse | undefined = undefined;
|
accessCode: string | undefined = undefined;
|
||||||
fingerprintPhrase: string | undefined = undefined;
|
|
||||||
privateKey: string | undefined = undefined;
|
privateKey: string | undefined = undefined;
|
||||||
publicKey: string | undefined = undefined;
|
|
||||||
|
|
||||||
static fromJSON(obj: Partial<Jsonify<LoginViaAuthRequestView>>): LoginViaAuthRequestView {
|
static fromJSON(obj: Partial<Jsonify<LoginViaAuthRequestView>>): LoginViaAuthRequestView | null {
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return Object.assign(new LoginViaAuthRequestView(), obj);
|
return Object.assign(new LoginViaAuthRequestView(), obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user