Skip to content

Commit aa02257

Browse files
Patrick-Pimentel-Bitwardentrmartin4
authored andcommitted
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 <[email protected]>
1 parent aa1e376 commit aa02257

File tree

8 files changed

+605
-357
lines changed

8 files changed

+605
-357
lines changed

apps/browser/src/platform/services/local-backed-session-storage.service.ts

-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ export class LocalBackedSessionStorageService
9090
this.logService.warning(
9191
`Possible unnecessary write to local session storage. Key: ${key}`,
9292
);
93-
this.logService.warning(obj as any);
9493
}
9594
} catch (err) {
9695
this.logService.warning(`Error while comparing values for key: ${key}`);

libs/angular/src/platform/abstractions/view-cache.service.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ type Deserializer<T> = {
99
* @param jsonValue The JSON object representation of your state.
1010
* @returns The fully typed version of your state.
1111
*/
12-
readonly deserializer?: (jsonValue: Jsonify<T>) => T;
12+
readonly deserializer?: (jsonValue: Jsonify<T>) => T | null;
1313
};
1414

1515
type BaseCacheOptions<T> = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
# Authentication Flows Documentation
2+
3+
## Standard Auth Request Flows
4+
5+
### Flow 1: Unauthed user requests approval from device; Approving device has a masterKey in memory
6+
7+
1. Unauthed user clicks "Login with device"
8+
2. Navigates to /login-with-device which creates a StandardAuthRequest
9+
3. Receives approval from a device with authRequestPublicKey(masterKey)
10+
4. Decrypts masterKey
11+
5. Decrypts userKey
12+
6. Proceeds to vault
13+
14+
### Flow 2: Unauthed user requests approval from device; Approving device does NOT have a masterKey in memory
15+
16+
1. Unauthed user clicks "Login with device"
17+
2. Navigates to /login-with-device which creates a StandardAuthRequest
18+
3. Receives approval from a device with authRequestPublicKey(userKey)
19+
4. Decrypts userKey
20+
5. Proceeds to vault
21+
22+
**Note:** This flow is an uncommon scenario and relates to TDE off-boarding. The following describes how a user could
23+
get into this flow:
24+
25+
1. An SSO TD user logs into a device via an Admin auth request approval, therefore this device does NOT have a masterKey
26+
in memory
27+
2. The org admin:
28+
- Changes the member decryption options from "Trusted devices" to "Master password" AND
29+
- Turns off the "Require single sign-on authentication" policy
30+
3. On another device, the user clicks "Login with device", which they can do because the org no longer requires SSO
31+
4. The user approves from the device they had previously logged into with SSO TD, which does NOT have a masterKey in
32+
memory
33+
34+
### Flow 3: Authed SSO TD user requests approval from device; Approving device has a masterKey in memory
35+
36+
1. SSO TD user authenticates via SSO
37+
2. Navigates to /login-initiated
38+
3. Clicks "Approve from your other device"
39+
4. Navigates to /login-with-device which creates a StandardAuthRequest
40+
5. Receives approval from device with authRequestPublicKey(masterKey)
41+
6. Decrypts masterKey
42+
7. Decrypts userKey
43+
8. Establishes trust (if required)
44+
9. Proceeds to vault
45+
46+
### Flow 4: Authed SSO TD user requests approval from device; Approving device does NOT have a masterKey in memory
47+
48+
1. SSO TD user authenticates via SSO
49+
2. Navigates to /login-initiated
50+
3. Clicks "Approve from your other device"
51+
4. Navigates to /login-with-device which creates a StandardAuthRequest
52+
5. Receives approval from device with authRequestPublicKey(userKey)
53+
6. Decrypts userKey
54+
7. Establishes trust (if required)
55+
8. Proceeds to vault
56+
57+
## Admin Auth Request Flow
58+
59+
### Flow: Authed SSO TD user requests admin approval
60+
61+
1. SSO TD user authenticates via SSO
62+
2. Navigates to /login-initiated
63+
3. Clicks "Request admin approval"
64+
4. Navigates to /admin-approval-requested which creates an AdminAuthRequest
65+
5. Receives approval from device with authRequestPublicKey(userKey)
66+
6. Decrypts userKey
67+
7. Establishes trust (if required)
68+
8. Proceeds to vault
69+
70+
**Note:** TDE users are required to be enrolled in admin account recovery, which gives the admin access to the user's
71+
userKey. This is how admins are able to send over the authRequestPublicKey(userKey) to the user to allow them to unlock.
72+
73+
## Summary Table
74+
75+
| Flow | Auth Status | Clicks Button [active route] | Navigates to | Approving device has masterKey in memory\* |
76+
| --------------- | ----------- | --------------------------------------------------- | ------------------------- | ------------------------------------------------- |
77+
| Standard Flow 1 | unauthed | "Login with device" [/login] | /login-with-device | yes |
78+
| Standard Flow 2 | unauthed | "Login with device" [/login] | /login-with-device | no |
79+
| Standard Flow 3 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | yes |
80+
| Standard Flow 4 | authed | "Approve from your other device" [/login-initiated] | /login-with-device | no |
81+
| Admin Flow | authed | "Request admin approval" [/login-initiated] | /admin-approval-requested | NA - admin requests always send encrypted userKey |
82+
83+
**Note:** The phrase "in memory" here is important. It is possible for a user to have a master password for their
84+
account, but not have a masterKey IN MEMORY for a specific device. For example, if a user registers an account with a
85+
master password, then joins an SSO TD org, then logs in to a device via SSO and admin auth request, they are now logged
86+
into that device but that device does not have masterKey IN MEMORY.
87+
88+
## State Management
89+
90+
### View Cache
91+
92+
The component uses `LoginViaAuthRequestCacheService` to manage persistent state across extension close and reopen.
93+
This cache stores:
94+
95+
- Auth Request ID
96+
- Private Key
97+
- Access Code
98+
99+
The cache is used to:
100+
101+
1. Preserve authentication state during extension close
102+
2. Allow resumption of pending auth requests
103+
3. Enable processing of approved requests after extension close and reopen.
104+
105+
### Component State Variables
106+
107+
Key state variables maintained during the authentication process:
108+
109+
#### Authentication Keys
110+
111+
```
112+
private authRequestKeyPair: {
113+
publicKey: Uint8Array | undefined;
114+
privateKey: Uint8Array | undefined;
115+
} | undefined
116+
```
117+
118+
- Stores the RSA key pair used for secure communication
119+
- Generated during auth request initialization
120+
- Required for decrypting approved auth responses
121+
122+
#### Access Code
123+
124+
```
125+
private accessCode: string | undefined
126+
```
127+
128+
- 25-character generated password
129+
- Used for retrieving auth responses when user is not authenticated
130+
- Required for standard auth flows
131+
132+
#### Authentication Status
133+
134+
```
135+
private authStatus: AuthenticationStatus | undefined
136+
```
137+
138+
- Tracks whether user is authenticated via SSO
139+
- Determines available flows and API endpoints
140+
- Affects navigation paths (`/login` vs `/login-initiated`)
141+
142+
#### Flow Control
143+
144+
```
145+
protected flow = Flow.StandardAuthRequest
146+
```
147+
148+
- Determines current authentication flow (Standard vs Admin)
149+
- Affects UI rendering and request handling
150+
- Set based on route and authentication state
151+
152+
### State Flow Examples
153+
154+
#### Standard Auth Request Cache Flow
155+
156+
1. User initiates login with device
157+
2. Component generates auth request and keys
158+
3. Cache service stores:
159+
```
160+
cacheLoginView(
161+
authRequestResponse.id,
162+
authRequestKeyPair.privateKey,
163+
accessCode
164+
)
165+
```
166+
4. On page refresh/revisit:
167+
- Component retrieves cached view
168+
- Reestablishes connection using cached credentials
169+
- Continues monitoring for approval
170+
171+
#### Admin Auth Request State Flow
172+
173+
1. User requests admin approval
174+
2. Component stores admin request in `AuthRequestService`:
175+
```
176+
setAdminAuthRequest(
177+
new AdminAuthRequestStorable({
178+
id: authRequestResponse.id,
179+
privateKey: authRequestKeyPair.privateKey
180+
}),
181+
userId
182+
)
183+
```
184+
3. On subsequent visits:
185+
- Component checks for existing admin requests
186+
- Either resumes monitoring or starts new request
187+
- Clears state after successful approval
188+
189+
### State Cleanup
190+
191+
State cleanup occurs in several scenarios:
192+
193+
- Component destruction (`ngOnDestroy`)
194+
- Successful authentication
195+
- Request denial or timeout
196+
- Manual navigation away
197+
198+
Key cleanup actions:
199+
200+
1. Hub connection termination
201+
2. Cache clearance
202+
3. Admin request state removal
203+
4. Key pair disposal
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,65 @@
1-
<div class="tw-text-center">
2-
<ng-container *ngIf="flow === Flow.StandardAuthRequest">
3-
<p *ngIf="clientType !== ClientType.Web">
4-
{{ "notificationSentDevicePart1" | i18n }}
5-
<a
6-
bitLink
7-
linkType="primary"
8-
class="tw-cursor-pointer"
9-
[href]="deviceManagementUrl"
10-
target="_blank"
11-
rel="noreferrer"
12-
>{{ "notificationSentDeviceAnchor" | i18n }}</a
13-
>. {{ "notificationSentDevicePart2" | i18n }}
14-
</p>
15-
<p *ngIf="clientType === ClientType.Web">
16-
{{ "notificationSentDeviceComplete" | i18n }}
17-
</p>
1+
<ng-container *ngIf="loading">
2+
<div class="tw-flex tw-items-center tw-justify-center">
3+
<i class="bwi bwi-spinner bwi-spin bwi-3x" aria-hidden="true"></i>
4+
</div>
5+
</ng-container>
186

19-
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
20-
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
7+
<ng-container *ngIf="!loading">
8+
<div class="tw-text-center">
9+
<ng-container *ngIf="flow === Flow.StandardAuthRequest">
10+
<p *ngIf="clientType !== ClientType.Web">
11+
{{ "notificationSentDevicePart1" | i18n }}
12+
<a
13+
bitLink
14+
linkType="primary"
15+
class="tw-cursor-pointer"
16+
[href]="deviceManagementUrl"
17+
target="_blank"
18+
rel="noreferrer"
19+
>{{ "notificationSentDeviceAnchor" | i18n }}</a
20+
>. {{ "notificationSentDevicePart2" | i18n }}
21+
</p>
22+
<p *ngIf="clientType === ClientType.Web">
23+
{{ "notificationSentDeviceComplete" | i18n }}
24+
</p>
2125

22-
<button
23-
*ngIf="showResendNotification"
24-
type="button"
25-
bitButton
26-
block
27-
buttonType="secondary"
28-
class="tw-mt-4"
29-
(click)="startStandardAuthRequestLogin(true)"
30-
>
31-
{{ "resendNotification" | i18n }}
32-
</button>
26+
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
27+
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
3328

34-
<div *ngIf="clientType !== ClientType.Browser" class="tw-mt-4">
35-
<span>{{ "needAnotherOptionV1" | i18n }}</span
36-
>&nbsp;
37-
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
38-
"viewAllLogInOptions" | i18n
39-
}}</a>
40-
</div>
41-
</ng-container>
29+
<button
30+
*ngIf="showResendNotification"
31+
type="button"
32+
bitButton
33+
block
34+
buttonType="secondary"
35+
class="tw-mt-4"
36+
(click)="handleNewStandardAuthRequestLogin()"
37+
>
38+
{{ "resendNotification" | i18n }}
39+
</button>
4240

43-
<ng-container *ngIf="flow === Flow.AdminAuthRequest">
44-
<p>{{ "youWillBeNotifiedOnceTheRequestIsApproved" | i18n }}</p>
41+
<div *ngIf="clientType !== ClientType.Browser" class="tw-mt-4">
42+
<span>{{ "needAnotherOptionV1" | i18n }}</span
43+
>&nbsp;
44+
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
45+
"viewAllLogInOptions" | i18n
46+
}}</a>
47+
</div>
48+
</ng-container>
4549

46-
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
47-
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
50+
<ng-container *ngIf="flow === Flow.AdminAuthRequest">
51+
<p>{{ "youWillBeNotifiedOnceTheRequestIsApproved" | i18n }}</p>
4852

49-
<div class="tw-mt-4">
50-
<span>{{ "troubleLoggingIn" | i18n }}</span
51-
>&nbsp;
52-
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
53-
"viewAllLogInOptions" | i18n
54-
}}</a>
55-
</div>
56-
</ng-container>
57-
</div>
53+
<div class="tw-font-semibold">{{ "fingerprintPhraseHeader" | i18n }}</div>
54+
<code class="tw-text-code">{{ fingerprintPhrase }}</code>
55+
56+
<div class="tw-mt-4">
57+
<span>{{ "troubleLoggingIn" | i18n }}</span
58+
>&nbsp;
59+
<a [routerLink]="backToRoute" bitLink linkType="primary">{{
60+
"viewAllLogInOptions" | i18n
61+
}}</a>
62+
</div>
63+
</ng-container>
64+
</div>
65+
</ng-container>

0 commit comments

Comments
 (0)