mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-28 12:45:45 +01:00
Observable auth statuses (#8537)
* Observable has token * Allow access to user key state observable * Create observable auth status * Fix DI
This commit is contained in:
parent
c3c895230f
commit
136226b6be
@ -24,6 +24,7 @@ import {
|
|||||||
} from "../../../platform/background/service-factories/state-service.factory";
|
} from "../../../platform/background/service-factories/state-service.factory";
|
||||||
|
|
||||||
import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory";
|
import { AccountServiceInitOptions, accountServiceFactory } from "./account-service.factory";
|
||||||
|
import { TokenServiceInitOptions, tokenServiceFactory } from "./token-service.factory";
|
||||||
|
|
||||||
type AuthServiceFactoryOptions = FactoryOptions;
|
type AuthServiceFactoryOptions = FactoryOptions;
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ export type AuthServiceInitOptions = AuthServiceFactoryOptions &
|
|||||||
MessagingServiceInitOptions &
|
MessagingServiceInitOptions &
|
||||||
CryptoServiceInitOptions &
|
CryptoServiceInitOptions &
|
||||||
ApiServiceInitOptions &
|
ApiServiceInitOptions &
|
||||||
StateServiceInitOptions;
|
StateServiceInitOptions &
|
||||||
|
TokenServiceInitOptions;
|
||||||
|
|
||||||
export function authServiceFactory(
|
export function authServiceFactory(
|
||||||
cache: { authService?: AbstractAuthService } & CachedServices,
|
cache: { authService?: AbstractAuthService } & CachedServices,
|
||||||
@ -49,6 +51,7 @@ export function authServiceFactory(
|
|||||||
await cryptoServiceFactory(cache, opts),
|
await cryptoServiceFactory(cache, opts),
|
||||||
await apiServiceFactory(cache, opts),
|
await apiServiceFactory(cache, opts),
|
||||||
await stateServiceFactory(cache, opts),
|
await stateServiceFactory(cache, opts),
|
||||||
|
await tokenServiceFactory(cache, opts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -579,6 +579,7 @@ export default class MainBackground {
|
|||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
this.tokenService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
this.billingAccountProfileStateService = new DefaultBillingAccountProfileStateService(
|
||||||
|
@ -503,6 +503,7 @@ export class Main {
|
|||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.apiService,
|
this.apiService,
|
||||||
this.stateService,
|
this.stateService,
|
||||||
|
this.tokenService,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
this.configApiService = new ConfigApiService(this.apiService, this.tokenService);
|
||||||
|
@ -349,6 +349,7 @@ const safeProviders: SafeProvider[] = [
|
|||||||
CryptoServiceAbstraction,
|
CryptoServiceAbstraction,
|
||||||
ApiServiceAbstraction,
|
ApiServiceAbstraction,
|
||||||
StateServiceAbstraction,
|
StateServiceAbstraction,
|
||||||
|
TokenService,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
safeProvider({
|
safeProvider({
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||||
|
|
||||||
export abstract class AuthService {
|
export abstract class AuthService {
|
||||||
/** Authentication status for the active user */
|
/** Authentication status for the active user */
|
||||||
abstract activeAccountStatus$: Observable<AuthenticationStatus>;
|
abstract activeAccountStatus$: Observable<AuthenticationStatus>;
|
||||||
|
/**
|
||||||
|
* Returns an observable authentication status for the given user id.
|
||||||
|
* @note userId is a required parameter, null values will always return `AuthenticationStatus.LoggedOut`
|
||||||
|
* @param userId The user id to check for an access token.
|
||||||
|
*/
|
||||||
|
abstract authStatusFor$(userId: UserId): Observable<AuthenticationStatus>;
|
||||||
/** @deprecated use {@link activeAccountStatus$} instead */
|
/** @deprecated use {@link activeAccountStatus$} instead */
|
||||||
abstract getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
|
abstract getAuthStatus: (userId?: string) => Promise<AuthenticationStatus>;
|
||||||
abstract logOut: (callback: () => void) => void;
|
abstract logOut: (callback: () => void) => void;
|
||||||
|
@ -1,8 +1,15 @@
|
|||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
import { DecodedAccessToken } from "../services/token.service";
|
import { DecodedAccessToken } from "../services/token.service";
|
||||||
|
|
||||||
export abstract class TokenService {
|
export abstract class TokenService {
|
||||||
|
/**
|
||||||
|
* Returns an observable that emits a boolean indicating whether the user has an access token.
|
||||||
|
* @param userId The user id to check for an access token.
|
||||||
|
*/
|
||||||
|
abstract hasAccessToken$(userId: UserId): Observable<boolean>;
|
||||||
/**
|
/**
|
||||||
* Sets the access token, refresh token, API Key Client ID, and API Key Client Secret in memory or disk
|
* Sets the access token, refresh token, API Key Client ID, and API Key Client Secret in memory or disk
|
||||||
* based on the given vaultTimeoutAction and vaultTimeout and the derived access token user id.
|
* based on the given vaultTimeoutAction and vaultTimeout and the derived access token user id.
|
||||||
|
@ -1,13 +1,21 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom, of } from "rxjs";
|
||||||
|
|
||||||
import { FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
import {
|
||||||
|
FakeAccountService,
|
||||||
|
makeStaticByteArray,
|
||||||
|
mockAccountServiceWith,
|
||||||
|
trackEmissions,
|
||||||
|
} from "../../../spec";
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateService } from "../../platform/abstractions/state.service";
|
||||||
import { Utils } from "../../platform/misc/utils";
|
import { Utils } from "../../platform/misc/utils";
|
||||||
|
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||||
import { UserId } from "../../types/guid";
|
import { UserId } from "../../types/guid";
|
||||||
|
import { UserKey } from "../../types/key";
|
||||||
|
import { TokenService } from "../abstractions/token.service";
|
||||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||||
|
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
@ -20,15 +28,18 @@ describe("AuthService", () => {
|
|||||||
let cryptoService: MockProxy<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let apiService: MockProxy<ApiService>;
|
let apiService: MockProxy<ApiService>;
|
||||||
let stateService: MockProxy<StateService>;
|
let stateService: MockProxy<StateService>;
|
||||||
|
let tokenService: MockProxy<TokenService>;
|
||||||
|
|
||||||
const userId = Utils.newGuid() as UserId;
|
const userId = Utils.newGuid() as UserId;
|
||||||
|
const userKey = new SymmetricCryptoKey(makeStaticByteArray(32) as Uint8Array) as UserKey;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
accountService = mockAccountServiceWith(userId);
|
accountService = mockAccountServiceWith(userId);
|
||||||
messagingService = mock<MessagingService>();
|
messagingService = mock();
|
||||||
cryptoService = mock<CryptoService>();
|
cryptoService = mock();
|
||||||
apiService = mock<ApiService>();
|
apiService = mock();
|
||||||
stateService = mock<StateService>();
|
stateService = mock();
|
||||||
|
tokenService = mock();
|
||||||
|
|
||||||
sut = new AuthService(
|
sut = new AuthService(
|
||||||
accountService,
|
accountService,
|
||||||
@ -36,26 +47,115 @@ describe("AuthService", () => {
|
|||||||
cryptoService,
|
cryptoService,
|
||||||
apiService,
|
apiService,
|
||||||
stateService,
|
stateService,
|
||||||
|
tokenService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("activeAccountStatus$", () => {
|
describe("activeAccountStatus$", () => {
|
||||||
test.each([
|
const accountInfo = {
|
||||||
AuthenticationStatus.LoggedOut,
|
status: AuthenticationStatus.Unlocked,
|
||||||
AuthenticationStatus.Locked,
|
|
||||||
AuthenticationStatus.Unlocked,
|
|
||||||
])(
|
|
||||||
`should emit %p when activeAccount$ emits an account with %p auth status`,
|
|
||||||
async (status) => {
|
|
||||||
accountService.activeAccountSubject.next({
|
|
||||||
id: userId,
|
id: userId,
|
||||||
email: "email",
|
email: "email",
|
||||||
name: "name",
|
name: "name",
|
||||||
status,
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
accountService.activeAccountSubject.next(accountInfo);
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(status);
|
it("emits LoggedOut when there is no active account", async () => {
|
||||||
},
|
accountService.activeAccountSubject.next(undefined);
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(
|
||||||
|
AuthenticationStatus.LoggedOut,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits LoggedOut when there is no access token", async () => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(false));
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(
|
||||||
|
AuthenticationStatus.LoggedOut,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits LoggedOut when there is no access token but has a user key", async () => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(false));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(
|
||||||
|
AuthenticationStatus.LoggedOut,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits Locked when there is an access token and no user key", async () => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Locked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits Unlocked when there is an access token and user key", async () => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.activeAccountStatus$)).toEqual(AuthenticationStatus.Unlocked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("follows the current active user", async () => {
|
||||||
|
const accountInfo2 = {
|
||||||
|
status: AuthenticationStatus.Unlocked,
|
||||||
|
id: Utils.newGuid() as UserId,
|
||||||
|
email: "email2",
|
||||||
|
name: "name2",
|
||||||
|
};
|
||||||
|
|
||||||
|
const emissions = trackEmissions(sut.activeAccountStatus$);
|
||||||
|
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
|
||||||
|
accountService.activeAccountSubject.next(accountInfo2);
|
||||||
|
|
||||||
|
expect(emissions).toEqual([AuthenticationStatus.Locked, AuthenticationStatus.Unlocked]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authStatusFor$", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits LoggedOut when userId is null", async () => {
|
||||||
|
expect(await firstValueFrom(sut.authStatusFor$(null))).toEqual(
|
||||||
|
AuthenticationStatus.LoggedOut,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits LoggedOut when there is no access token", async () => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(false));
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(
|
||||||
|
AuthenticationStatus.LoggedOut,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits Locked when there is an access token and no user key", async () => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(undefined));
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(AuthenticationStatus.Locked);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("emits Unlocked when there is an access token and user key", async () => {
|
||||||
|
tokenService.hasAccessToken$.mockReturnValue(of(true));
|
||||||
|
cryptoService.getInMemoryUserKeyFor$.mockReturnValue(of(userKey));
|
||||||
|
|
||||||
|
expect(await firstValueFrom(sut.authStatusFor$(userId))).toEqual(
|
||||||
|
AuthenticationStatus.Unlocked,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
import { Observable, distinctUntilChanged, map, shareReplay } from "rxjs";
|
import {
|
||||||
|
Observable,
|
||||||
|
combineLatest,
|
||||||
|
distinctUntilChanged,
|
||||||
|
map,
|
||||||
|
of,
|
||||||
|
shareReplay,
|
||||||
|
switchMap,
|
||||||
|
} from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "../../abstractions/api.service";
|
import { ApiService } from "../../abstractions/api.service";
|
||||||
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
import { CryptoService } from "../../platform/abstractions/crypto.service";
|
||||||
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
import { MessagingService } from "../../platform/abstractions/messaging.service";
|
||||||
import { StateService } from "../../platform/abstractions/state.service";
|
import { StateService } from "../../platform/abstractions/state.service";
|
||||||
import { KeySuffixOptions } from "../../platform/enums";
|
import { KeySuffixOptions } from "../../platform/enums";
|
||||||
|
import { UserId } from "../../types/guid";
|
||||||
import { AccountService } from "../abstractions/account.service";
|
import { AccountService } from "../abstractions/account.service";
|
||||||
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
import { AuthService as AuthServiceAbstraction } from "../abstractions/auth.service";
|
||||||
|
import { TokenService } from "../abstractions/token.service";
|
||||||
import { AuthenticationStatus } from "../enums/authentication-status";
|
import { AuthenticationStatus } from "../enums/authentication-status";
|
||||||
|
|
||||||
export class AuthService implements AuthServiceAbstraction {
|
export class AuthService implements AuthServiceAbstraction {
|
||||||
@ -18,9 +28,36 @@ export class AuthService implements AuthServiceAbstraction {
|
|||||||
protected cryptoService: CryptoService,
|
protected cryptoService: CryptoService,
|
||||||
protected apiService: ApiService,
|
protected apiService: ApiService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
|
private tokenService: TokenService,
|
||||||
) {
|
) {
|
||||||
this.activeAccountStatus$ = this.accountService.activeAccount$.pipe(
|
this.activeAccountStatus$ = this.accountService.activeAccount$.pipe(
|
||||||
map((account) => account.status),
|
map((account) => account?.id),
|
||||||
|
switchMap((userId) => {
|
||||||
|
return this.authStatusFor$(userId);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
authStatusFor$(userId: UserId): Observable<AuthenticationStatus> {
|
||||||
|
if (userId == null) {
|
||||||
|
return of(AuthenticationStatus.LoggedOut);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineLatest([
|
||||||
|
this.cryptoService.getInMemoryUserKeyFor$(userId),
|
||||||
|
this.tokenService.hasAccessToken$(userId),
|
||||||
|
]).pipe(
|
||||||
|
map(([userKey, hasAccessToken]) => {
|
||||||
|
if (!hasAccessToken) {
|
||||||
|
return AuthenticationStatus.LoggedOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userKey) {
|
||||||
|
return AuthenticationStatus.Locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthenticationStatus.Unlocked;
|
||||||
|
}),
|
||||||
distinctUntilChanged(),
|
distinctUntilChanged(),
|
||||||
shareReplay({ bufferSize: 1, refCount: false }),
|
shareReplay({ bufferSize: 1, refCount: false }),
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { MockProxy, mock } from "jest-mock-extended";
|
import { MockProxy, mock } from "jest-mock-extended";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec";
|
import { FakeSingleUserStateProvider, FakeGlobalStateProvider } from "../../../spec";
|
||||||
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum";
|
||||||
@ -104,6 +105,61 @@ describe("TokenService", () => {
|
|||||||
const accessTokenKeyPartialSecureStorageKey = `_accessTokenKey`;
|
const accessTokenKeyPartialSecureStorageKey = `_accessTokenKey`;
|
||||||
const accessTokenKeySecureStorageKey = `${userIdFromAccessToken}${accessTokenKeyPartialSecureStorageKey}`;
|
const accessTokenKeySecureStorageKey = `${userIdFromAccessToken}${accessTokenKeyPartialSecureStorageKey}`;
|
||||||
|
|
||||||
|
describe("hasAccessToken$", () => {
|
||||||
|
it("returns true when an access token exists in memory", async () => {
|
||||||
|
// Arrange
|
||||||
|
singleUserStateProvider
|
||||||
|
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
|
||||||
|
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when an access token exists in disk", async () => {
|
||||||
|
// Arrange
|
||||||
|
singleUserStateProvider
|
||||||
|
.getFake(userIdFromAccessToken, ACCESS_TOKEN_MEMORY)
|
||||||
|
.stateSubject.next([userIdFromAccessToken, undefined]);
|
||||||
|
|
||||||
|
singleUserStateProvider
|
||||||
|
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
|
||||||
|
.stateSubject.next([userIdFromAccessToken, accessTokenJwt]);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true when an access token exists in secure storage", async () => {
|
||||||
|
// Arrange
|
||||||
|
singleUserStateProvider
|
||||||
|
.getFake(userIdFromAccessToken, ACCESS_TOKEN_DISK)
|
||||||
|
.stateSubject.next([userIdFromAccessToken, "encryptedAccessToken"]);
|
||||||
|
|
||||||
|
secureStorageService.get.mockResolvedValue(accessTokenKeyB64);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false if no access token exists in memory, disk, or secure storage", async () => {
|
||||||
|
// Act
|
||||||
|
const result = await firstValueFrom(tokenService.hasAccessToken$(userIdFromAccessToken));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("setAccessToken", () => {
|
describe("setAccessToken", () => {
|
||||||
it("should throw an error if the access token is null", async () => {
|
it("should throw an error if the access token is null", async () => {
|
||||||
// Act
|
// Act
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { Observable, combineLatest, firstValueFrom, map } from "rxjs";
|
||||||
import { Opaque } from "type-fest";
|
import { Opaque } from "type-fest";
|
||||||
|
|
||||||
import { decodeJwtTokenToJson } from "@bitwarden/auth/common";
|
import { decodeJwtTokenToJson } from "@bitwarden/auth/common";
|
||||||
@ -135,6 +135,15 @@ export class TokenService implements TokenServiceAbstraction {
|
|||||||
this.initializeState();
|
this.initializeState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasAccessToken$(userId: UserId): Observable<boolean> {
|
||||||
|
// FIXME Once once vault timeout action is observable, we can use it to determine storage location
|
||||||
|
// and avoid the need to check both disk and memory.
|
||||||
|
return combineLatest([
|
||||||
|
this.singleUserStateProvider.get(userId, ACCESS_TOKEN_DISK).state$,
|
||||||
|
this.singleUserStateProvider.get(userId, ACCESS_TOKEN_MEMORY).state$,
|
||||||
|
]).pipe(map(([disk, memory]) => Boolean(disk || memory)));
|
||||||
|
}
|
||||||
|
|
||||||
// pivoting to an approach where we create a symmetric key we store in secure storage
|
// pivoting to an approach where we create a symmetric key we store in secure storage
|
||||||
// which is used to protect the data before persisting to disk.
|
// which is used to protect the data before persisting to disk.
|
||||||
// We will also use the same symmetric key to decrypt the data when reading from disk.
|
// We will also use the same symmetric key to decrypt the data when reading from disk.
|
||||||
|
@ -13,6 +13,14 @@ import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
|
|||||||
|
|
||||||
export abstract class CryptoService {
|
export abstract class CryptoService {
|
||||||
abstract activeUserKey$: Observable<UserKey>;
|
abstract activeUserKey$: Observable<UserKey>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the an observable key for the given user id.
|
||||||
|
*
|
||||||
|
* @note this observable represents only user keys stored in memory. A null value does not indicate that we cannot load a user key from storage.
|
||||||
|
* @param userId The desired user
|
||||||
|
*/
|
||||||
|
abstract getInMemoryUserKeyFor$(userId: UserId): Observable<UserKey>;
|
||||||
/**
|
/**
|
||||||
* Sets the provided user key and stores
|
* Sets the provided user key and stores
|
||||||
* any other necessary versions (such as auto, biometrics,
|
* any other necessary versions (such as auto, biometrics,
|
||||||
|
@ -160,6 +160,10 @@ export class CryptoService implements CryptoServiceAbstraction {
|
|||||||
await this.setUserKey(key);
|
await this.setUserKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInMemoryUserKeyFor$(userId: UserId): Observable<UserKey> {
|
||||||
|
return this.stateProvider.getUserState$(USER_KEY, userId);
|
||||||
|
}
|
||||||
|
|
||||||
async getUserKey(userId?: UserId): Promise<UserKey> {
|
async getUserKey(userId?: UserId): Promise<UserKey> {
|
||||||
let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
|
let userKey = await firstValueFrom(this.stateProvider.getUserState$(USER_KEY, userId));
|
||||||
if (userKey) {
|
if (userKey) {
|
||||||
|
Loading…
Reference in New Issue
Block a user