1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-18 09:13:33 +00:00

Revert "Revert "Auth/PM-6689 - Migrate Security Stamp to Token Service and St…" (#8889)

This reverts commit 100b43dd8f.
This commit is contained in:
Jared Snider
2024-04-24 12:37:19 -04:00
committed by GitHub
parent 94fe9bd053
commit a12c140792
16 changed files with 126 additions and 63 deletions

View File

@@ -213,4 +213,10 @@ export abstract class TokenService {
* @returns A promise that resolves with a boolean representing the user's external authN status.
*/
getIsExternal: () => Promise<boolean>;
/** Gets the active or passed in user's security stamp */
getSecurityStamp: (userId?: UserId) => Promise<string | null>;
/** Sets the security stamp for the active or passed in user */
setSecurityStamp: (securityStamp: string, userId?: UserId) => Promise<void>;
}

View File

@@ -23,6 +23,7 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY,
SECURITY_STAMP_MEMORY,
} from "./token.state";
describe("TokenService", () => {
@@ -2191,6 +2192,84 @@ describe("TokenService", () => {
});
});
describe("Security Stamp methods", () => {
const mockSecurityStamp = "securityStamp";
describe("setSecurityStamp", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.setSecurityStamp(mockSecurityStamp);
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot set security stamp.");
});
it("should set the security stamp in memory when there is an active user in global state", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
// Act
await tokenService.setSecurityStamp(mockSecurityStamp);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock,
).toHaveBeenCalledWith(mockSecurityStamp);
});
it("should set the security stamp in memory for the specified user id", async () => {
// Act
await tokenService.setSecurityStamp(mockSecurityStamp, userIdFromAccessToken);
// Assert
expect(
singleUserStateProvider.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY).nextMock,
).toHaveBeenCalledWith(mockSecurityStamp);
});
});
describe("getSecurityStamp", () => {
it("should throw an error if no user id is provided and there is no active user in global state", async () => {
// Act
// note: don't await here because we want to test the error
const result = tokenService.getSecurityStamp();
// Assert
await expect(result).rejects.toThrow("User id not found. Cannot get security stamp.");
});
it("should return the security stamp from memory with no user id specified (uses global active user)", async () => {
// Arrange
globalStateProvider
.getFake(ACCOUNT_ACTIVE_ACCOUNT_ID)
.stateSubject.next(userIdFromAccessToken);
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
.stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
// Act
const result = await tokenService.getSecurityStamp();
// Assert
expect(result).toEqual(mockSecurityStamp);
});
it("should return the security stamp from memory for the specified user id", async () => {
// Arrange
singleUserStateProvider
.getFake(userIdFromAccessToken, SECURITY_STAMP_MEMORY)
.stateSubject.next([userIdFromAccessToken, mockSecurityStamp]);
// Act
const result = await tokenService.getSecurityStamp(userIdFromAccessToken);
// Assert
expect(result).toEqual(mockSecurityStamp);
});
});
});
// Helpers
function createTokenService(supportsSecureStorage: boolean) {
return new TokenService(

View File

@@ -32,6 +32,7 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY,
SECURITY_STAMP_MEMORY,
} from "./token.state";
export enum TokenStorageLocation {
@@ -850,6 +851,30 @@ export class TokenService implements TokenServiceAbstraction {
return Array.isArray(decoded.amr) && decoded.amr.includes("external");
}
async getSecurityStamp(userId?: UserId): Promise<string | null> {
userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$);
if (!userId) {
throw new Error("User id not found. Cannot get security stamp.");
}
const securityStamp = await this.getStateValueByUserIdAndKeyDef(userId, SECURITY_STAMP_MEMORY);
return securityStamp;
}
async setSecurityStamp(securityStamp: string, userId?: UserId): Promise<void> {
userId ??= await firstValueFrom(this.activeUserIdGlobalState.state$);
if (!userId) {
throw new Error("User id not found. Cannot set security stamp.");
}
await this.singleUserStateProvider
.get(userId, SECURITY_STAMP_MEMORY)
.update((_) => securityStamp);
}
private async getStateValueByUserIdAndKeyDef(
userId: UserId,
storageLocation: UserKeyDefinition<string>,

View File

@@ -10,6 +10,7 @@ import {
EMAIL_TWO_FACTOR_TOKEN_RECORD_DISK_LOCAL,
REFRESH_TOKEN_DISK,
REFRESH_TOKEN_MEMORY,
SECURITY_STAMP_MEMORY,
} from "./token.state";
describe.each([
@@ -22,6 +23,7 @@ describe.each([
[API_KEY_CLIENT_ID_MEMORY, "apiKeyClientIdMemory"],
[API_KEY_CLIENT_SECRET_DISK, "apiKeyClientSecretDisk"],
[API_KEY_CLIENT_SECRET_MEMORY, "apiKeyClientSecretMemory"],
[SECURITY_STAMP_MEMORY, "securityStamp"],
])(
"deserializes state key definitions",
(

View File

@@ -69,3 +69,8 @@ export const API_KEY_CLIENT_SECRET_MEMORY = new UserKeyDefinition<string>(
clearOn: [], // Manually handled
},
);
export const SECURITY_STAMP_MEMORY = new UserKeyDefinition<string>(TOKEN_MEMORY, "securityStamp", {
deserializer: (securityStamp) => securityStamp,
clearOn: ["logout"],
});