mirror of
https://github.com/bitwarden/browser
synced 2026-02-10 21:50:15 +00:00
fix(device-approval-persistence): [PM-19380] Device Approval Persistence - Added hella documentation, was very needed.
This commit is contained in:
@@ -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 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\* |
|
||||
| --------------- | ----------- | --------------------------------------------------- | ------------------------- | ------------------------------------------------- |
|
||||
| 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 page refreshes. 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 navigation/refresh
|
||||
|
||||
### 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/navigation:
|
||||
- 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
|
||||
@@ -194,11 +194,18 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if we have a cached auth request to try to process
|
||||
// [Standard Flow State Management] Check cached auth request
|
||||
const cachedAuthRequest: LoginViaAuthRequestView | null =
|
||||
this.loginViaAuthRequestCacheService.getCachedLoginViaAuthRequestView();
|
||||
|
||||
if (cachedAuthRequest) {
|
||||
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.processAuthRequestResponse(cachedAuthRequest.id);
|
||||
} else {
|
||||
@@ -223,6 +230,14 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
|
||||
private async startAdminAuthRequestLogin(): Promise<void> {
|
||||
try {
|
||||
if (!this.email) {
|
||||
this.logService.error("No email when starting admin auth request login.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Scope this auth request to just this process. We don't want it to carry
|
||||
// on outside of this scope because it should either be regenerated with
|
||||
// what is in the cache or created initially like it is doing here.
|
||||
const authRequest = await this.buildAuthRequest(this.email, AuthRequestType.AdminApproval);
|
||||
|
||||
if (!authRequest) {
|
||||
@@ -282,6 +297,11 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cachedAuthRequest.privateKey) {
|
||||
this.logService.error("No private key on the cached auth request.");
|
||||
return;
|
||||
}
|
||||
|
||||
const privateKey = Utils.fromB64ToArray(cachedAuthRequest.privateKey);
|
||||
|
||||
// Re-derive the user's fingerprint phrase
|
||||
@@ -307,6 +327,11 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
this.showResendNotification = false;
|
||||
|
||||
try {
|
||||
if (!this.email) {
|
||||
this.logService.error("Email not defined when starting standard auth request login.");
|
||||
return;
|
||||
}
|
||||
|
||||
const authRequest = await this.buildAuthRequest(
|
||||
this.email,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
@@ -334,6 +359,16 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
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;
|
||||
}
|
||||
|
||||
if (!this.accessCode) {
|
||||
this.logService.error("No access code when trying to cache the login view.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.loginViaAuthRequestCacheService.cacheLoginView(
|
||||
authRequestResponse.id,
|
||||
this.authRequestKeyPair.privateKey,
|
||||
@@ -366,6 +401,12 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
|
||||
const deviceIdentifier = await this.appIdService.getAppId();
|
||||
|
||||
if (!this.authRequestKeyPair.publicKey) {
|
||||
const errorMessage = "No public key when building an auth request.";
|
||||
this.logService.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
this.fingerprintPhrase = await this.authRequestService.getFingerprintPhrase(
|
||||
email,
|
||||
this.authRequestKeyPair.publicKey,
|
||||
@@ -447,9 +488,15 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
await this.anonymousHubService.createHubConnection(adminAuthRequestStorable.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used for trying to get the auth request back out of state.
|
||||
* @param requestId
|
||||
* @private
|
||||
*/
|
||||
private async retrieveAuthRequest(requestId: string): Promise<AuthRequestResponse> {
|
||||
let authRequestResponse: AuthRequestResponse;
|
||||
let authRequestResponse: AuthRequestResponse | undefined = undefined;
|
||||
try {
|
||||
// There are two cases here, the first being
|
||||
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
|
||||
@@ -458,8 +505,9 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
authRequestResponse = await this.authRequestApiService.getAuthRequest(requestId);
|
||||
} else {
|
||||
if (!this.accessCode) {
|
||||
this.logService.error("No access code available when handling approved auth request.");
|
||||
return;
|
||||
const errorMessage = "No access code available when handling approved auth request.";
|
||||
this.logService.error(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
authRequestResponse = await this.authRequestApiService.getAuthResponse(
|
||||
requestId,
|
||||
@@ -469,82 +517,25 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
} 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) {
|
||||
return null;
|
||||
this.logService.error(error.message);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (authRequestResponse === undefined) {
|
||||
throw new Error("Auth reqeust response not generated");
|
||||
}
|
||||
|
||||
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
|
||||
* 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 processAuthRequestResponse(requestId: string): Promise<void> {
|
||||
/**
|
||||
* ***********************************
|
||||
* Standard Auth Request Flows
|
||||
* ***********************************
|
||||
*
|
||||
* Flow 1: Unauthed user requests approval from device; Approving device has 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(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 {
|
||||
const authRequestResponse = await this.retrieveAuthRequest(requestId);
|
||||
|
||||
@@ -562,10 +553,10 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
if (authRequestResponse.requestApproved) {
|
||||
const userHasAuthenticatedViaSSO = this.authStatus === AuthenticationStatus.Locked;
|
||||
if (userHasAuthenticatedViaSSO) {
|
||||
// Handles Standard Flows 3-4 and Admin Flow
|
||||
// [Standard Flow 3-4] Handle authenticated SSO TD user flows
|
||||
return await this.handleAuthenticatedFlows(authRequestResponse);
|
||||
} else {
|
||||
// Handles Standard Flows 1-2
|
||||
// [Standard Flow 1-2] Handle unauthenticated user flows
|
||||
return await this.handleUnauthenticatedFlows(authRequestResponse, requestId);
|
||||
}
|
||||
}
|
||||
@@ -582,6 +573,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
private async handleAuthenticatedFlows(authRequestResponse: AuthRequestResponse) {
|
||||
// [Standard Flow 3-4] Handle authenticated SSO TD user flows
|
||||
const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id;
|
||||
if (!userId) {
|
||||
this.logService.error(
|
||||
@@ -606,6 +598,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
authRequestResponse: AuthRequestResponse,
|
||||
requestId: string,
|
||||
) {
|
||||
// [Standard Flow 1-2] Handle unauthenticated user flows
|
||||
const authRequestLoginCredentials = await this.buildAuthRequestLoginCredentials(
|
||||
requestId,
|
||||
authRequestResponse,
|
||||
@@ -628,21 +621,20 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
userId: UserId,
|
||||
): Promise<void> {
|
||||
/**
|
||||
* See verifyAndHandleApprovedAuthReq() for flow details.
|
||||
*
|
||||
* [Flow Type Detection]
|
||||
* 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` does not have a value, we receive the `key` as an authRequestPublicKey(userKey)
|
||||
* - If `masterPasswordHash` exists: Standard Flow 1 or 3 (device has masterKey)
|
||||
* - If no `masterPasswordHash`: Standard Flow 2, 4, or Admin Flow (device sends userKey)
|
||||
*/
|
||||
if (authRequestResponse.masterPasswordHash) {
|
||||
// ...in Standard Auth Request Flow 3
|
||||
// [Standard Flow 1 or 3] Device has masterKey
|
||||
await this.authRequestService.setKeysAfterDecryptingSharedMasterKeyAndHash(
|
||||
authRequestResponse,
|
||||
privateKey,
|
||||
userId,
|
||||
);
|
||||
} 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(
|
||||
authRequestResponse,
|
||||
privateKey,
|
||||
@@ -650,6 +642,7 @@ 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)
|
||||
// TODO: this should eventually be enforced via deleting this on the server once it is used
|
||||
await this.authRequestService.clearAdminAuthRequest(userId);
|
||||
@@ -659,6 +652,7 @@ export class LoginViaAuthRequestComponent implements OnInit, OnDestroy {
|
||||
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
|
||||
// need to establish trust on the current device
|
||||
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
|
||||
|
||||
Reference in New Issue
Block a user