1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-21 03:43:41 +00:00

[PM-23243] In sync response and identity success response add MasterPasswordUnlockDataResponse in decryption options response model. (#15916)

* added master password unlock and decryption option fields into identity token connect response

* incorrect master password unlock response parsing

* use sdk

* use sdk

* better type checking on response parsing

* not using sdk

* revert of bad merge conflicts

* revert of bad merge conflicts

* master password unlock setter in state

* unit test coverage for responses processing

* master password unlock in identity user decryption options

* unit test coverage

* unit test coverage

* unit test coverage

* unit test coverage

* lint error

* set master password unlock data in state on identity response and sync response

* revert change in auth's user decryption options

* remove unnecessary cast

* better docs

* change to relative imports

* MasterPasswordUnlockData serialization issue

* explicit undefined type for `syncUserDecryption`

* incorrect identity token response tests
This commit is contained in:
Maciej Zieniuk
2025-09-05 16:13:56 +02:00
committed by GitHub
parent 6c5e15eb28
commit 203a24723b
24 changed files with 852 additions and 37 deletions

View File

@@ -0,0 +1,119 @@
import { makeEncString } from "../../../../spec";
import { IdentityTokenResponse } from "./identity-token.response";
describe("IdentityTokenResponse", () => {
const accessToken = "testAccessToken";
const tokenType = "Bearer";
const expiresIn = 3600;
const refreshToken = "testRefreshToken";
const encryptedUserKey = makeEncString("testUserKey");
it("should throw an error when access token is missing", () => {
const response = {
access_token: undefined as unknown,
token_type: tokenType,
};
expect(() => new IdentityTokenResponse(response)).toThrow(
"Identity response does not contain a valid access token",
);
});
it("should throw an error when token type is missing", () => {
const response = {
access_token: accessToken,
token_type: undefined as unknown,
};
expect(() => new IdentityTokenResponse(response)).toThrow(
"Identity response does not contain a valid token type",
);
});
it("should create response without optional fields", () => {
const response = {
access_token: accessToken,
token_type: tokenType,
};
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.accessToken).toEqual(accessToken);
expect(identityTokenResponse.tokenType).toEqual(tokenType);
expect(identityTokenResponse.expiresIn).toBeUndefined();
expect(identityTokenResponse.refreshToken).toBeUndefined();
});
it("should create response with expires_in present", () => {
const response = {
access_token: accessToken,
token_type: tokenType,
expires_in: expiresIn,
};
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.accessToken).toEqual(accessToken);
expect(identityTokenResponse.tokenType).toEqual(tokenType);
expect(identityTokenResponse.expiresIn).toEqual(expiresIn);
expect(identityTokenResponse.refreshToken).toBeUndefined();
});
it("should create response with refresh_token present", () => {
const response = {
access_token: accessToken,
token_type: tokenType,
expires_in: expiresIn,
refresh_token: refreshToken,
};
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.accessToken).toEqual(accessToken);
expect(identityTokenResponse.tokenType).toEqual(tokenType);
expect(identityTokenResponse.expiresIn).toEqual(expiresIn);
expect(identityTokenResponse.refreshToken).toEqual(refreshToken);
});
it("should create response with key is not present", () => {
const response = {
access_token: accessToken,
token_type: tokenType,
Key: undefined as unknown,
};
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.key).toBeUndefined();
});
it("should create response with key present", () => {
const response = {
access_token: accessToken,
token_type: tokenType,
Key: encryptedUserKey.encryptedString,
};
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.key).toEqual(encryptedUserKey);
});
it("should create response with user decryption options is not present", () => {
const response = {
access_token: accessToken,
token_type: tokenType,
UserDecryptionOptions: undefined as unknown,
};
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.userDecryptionOptions).toBeUndefined();
});
it("should create response with user decryption options present", () => {
const response = {
access_token: accessToken,
token_type: tokenType,
UserDecryptionOptions: {},
};
const identityTokenResponse = new IdentityTokenResponse(response);
expect(identityTokenResponse.userDecryptionOptions).toBeDefined();
});
});

View File

@@ -12,8 +12,8 @@ import { UserDecryptionOptionsResponse } from "./user-decryption-options/user-de
export class IdentityTokenResponse extends BaseResponse {
accessToken: string;
expiresIn: number;
refreshToken: string;
expiresIn?: number;
refreshToken?: string;
tokenType: string;
resetMasterPassword: boolean;
@@ -26,14 +26,30 @@ export class IdentityTokenResponse extends BaseResponse {
apiUseKeyConnector: boolean;
keyConnectorUrl: string;
userDecryptionOptions: UserDecryptionOptionsResponse;
userDecryptionOptions?: UserDecryptionOptionsResponse;
constructor(response: any) {
constructor(response: unknown) {
super(response);
this.accessToken = response.access_token;
this.expiresIn = response.expires_in;
this.refreshToken = response.refresh_token;
this.tokenType = response.token_type;
const accessToken = this.getResponseProperty("access_token");
if (accessToken == null || typeof accessToken !== "string") {
throw new Error("Identity response does not contain a valid access token");
}
const tokenType = this.getResponseProperty("token_type");
if (tokenType == null || typeof tokenType !== "string") {
throw new Error("Identity response does not contain a valid token type");
}
this.accessToken = accessToken;
this.tokenType = tokenType;
const expiresIn = this.getResponseProperty("expires_in");
if (expiresIn != null && typeof expiresIn === "number") {
this.expiresIn = expiresIn;
}
const refreshToken = this.getResponseProperty("refresh_token");
if (refreshToken != null && typeof refreshToken === "string") {
this.refreshToken = refreshToken;
}
this.resetMasterPassword = this.getResponseProperty("ResetMasterPassword");
this.privateKey = this.getResponseProperty("PrivateKey");
@@ -57,10 +73,9 @@ export class IdentityTokenResponse extends BaseResponse {
this.getResponseProperty("MasterPasswordPolicy"),
);
if (response.UserDecryptionOptions) {
this.userDecryptionOptions = new UserDecryptionOptionsResponse(
this.getResponseProperty("UserDecryptionOptions"),
);
const userDecryptionOptions = this.getResponseProperty("UserDecryptionOptions");
if (userDecryptionOptions != null && typeof userDecryptionOptions === "object") {
this.userDecryptionOptions = new UserDecryptionOptionsResponse(userDecryptionOptions);
}
}

View File

@@ -0,0 +1,47 @@
// eslint-disable-next-line no-restricted-imports
import { KdfType } from "@bitwarden/key-management";
import { makeEncString } from "../../../../../spec";
import { UserDecryptionOptionsResponse } from "./user-decryption-options.response";
describe("UserDecryptionOptionsResponse", () => {
it("should create response when master password unlock is present", () => {
const salt = "test@example.com";
const encryptedUserKey = makeEncString("testUserKey");
const response = new UserDecryptionOptionsResponse({
HasMasterPassword: true,
MasterPasswordUnlock: {
Salt: salt,
Kdf: {
KdfType: KdfType.PBKDF2_SHA256,
Iterations: 600_000,
},
MasterKeyEncryptedUserKey: encryptedUserKey.encryptedString,
},
});
expect(response.hasMasterPassword).toBe(true);
expect(response.masterPasswordUnlock).toBeDefined();
expect(response.masterPasswordUnlock!.salt).toEqual(salt);
expect(response.masterPasswordUnlock!.kdf.kdfType).toEqual(KdfType.PBKDF2_SHA256);
expect(response.masterPasswordUnlock!.kdf.iterations).toEqual(600_000);
expect(response.masterPasswordUnlock!.masterKeyWrappedUserKey).toEqual(encryptedUserKey);
expect(response.trustedDeviceOption).toBeUndefined();
expect(response.keyConnectorOption).toBeUndefined();
expect(response.webAuthnPrfOption).toBeUndefined();
});
it("should create response when master password unlock is not present", () => {
const response = new UserDecryptionOptionsResponse({
HasMasterPassword: false,
});
expect(response.hasMasterPassword).toBe(false);
expect(response.masterPasswordUnlock).toBeUndefined();
expect(response.trustedDeviceOption).toBeUndefined();
expect(response.keyConnectorOption).toBeUndefined();
expect(response.webAuthnPrfOption).toBeUndefined();
});
});

View File

@@ -1,3 +1,4 @@
import { MasterPasswordUnlockResponse } from "../../../../key-management/master-password/models/response/master-password-unlock.response";
import { BaseResponse } from "../../../../models/response/base.response";
import {
@@ -15,6 +16,7 @@ import {
export interface IUserDecryptionOptionsServerResponse {
HasMasterPassword: boolean;
MasterPasswordUnlock?: unknown;
TrustedDeviceOption?: ITrustedDeviceUserDecryptionOptionServerResponse;
KeyConnectorOption?: IKeyConnectorUserDecryptionOptionServerResponse;
WebAuthnPrfOption?: IWebAuthnPrfDecryptionOptionServerResponse;
@@ -22,6 +24,7 @@ export interface IUserDecryptionOptionsServerResponse {
export class UserDecryptionOptionsResponse extends BaseResponse {
hasMasterPassword: boolean;
masterPasswordUnlock?: MasterPasswordUnlockResponse;
trustedDeviceOption?: TrustedDeviceUserDecryptionOptionResponse;
keyConnectorOption?: KeyConnectorUserDecryptionOptionResponse;
webAuthnPrfOption?: WebAuthnPrfDecryptionOptionResponse;
@@ -31,6 +34,11 @@ export class UserDecryptionOptionsResponse extends BaseResponse {
this.hasMasterPassword = this.getResponseProperty("HasMasterPassword");
const masterPasswordUnlock = this.getResponseProperty("MasterPasswordUnlock");
if (masterPasswordUnlock != null && typeof masterPasswordUnlock === "object") {
this.masterPasswordUnlock = new MasterPasswordUnlockResponse(masterPasswordUnlock);
}
if (response.TrustedDeviceOption) {
this.trustedDeviceOption = new TrustedDeviceUserDecryptionOptionResponse(
this.getResponseProperty("TrustedDeviceOption"),