Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions __tests__/services/auth.utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
migrateCookiesToLocalStorage,
removeAuthJwt,
setAuthJwt,
syncWalletRoleWithServer,
} from "@/services/auth/auth.utils";

jest.mock("js-cookie", () => ({
Expand Down Expand Up @@ -50,6 +51,44 @@ describe("auth.utils", () => {
"6529-wallet-role",
"role"
);
expect(safeLocalStorage.setItem).toHaveBeenCalledWith(
"auth-role-addr",
"role"
);
});

it("setAuthJwt clears role storage when role is missing", () => {
(jwtDecode as jest.Mock).mockReturnValue({ exp: 86400 * 2 });
jest.spyOn(Date, "now").mockReturnValue(0);
setAuthJwt("Addr", "jwt", "refresh");
expect(safeLocalStorage.removeItem).toHaveBeenCalledWith(
"6529-wallet-role"
);
expect(safeLocalStorage.removeItem).toHaveBeenCalledWith(
"auth-role-addr"
);
});

it("syncWalletRoleWithServer stores server role", () => {
syncWalletRoleWithServer("Admin", "0xABC");
expect(safeLocalStorage.setItem).toHaveBeenCalledWith(
"6529-wallet-role",
"Admin"
);
expect(safeLocalStorage.setItem).toHaveBeenCalledWith(
"auth-role-0xabc",
"Admin"
);
});

it("syncWalletRoleWithServer clears role when server role is missing", () => {
syncWalletRoleWithServer(null, "0xAbC");
expect(safeLocalStorage.removeItem).toHaveBeenCalledWith(
"6529-wallet-role"
);
expect(safeLocalStorage.removeItem).toHaveBeenCalledWith(
"auth-role-0xabc"
);
});

it("getAuthJwt prefers dev mode", () => {
Expand Down Expand Up @@ -105,6 +144,7 @@ describe("auth.utils", () => {
});

it("removeAuthJwt clears storage and cookie", () => {
(safeLocalStorage.getItem as jest.Mock).mockReturnValue("Addr");
removeAuthJwt();
expect(Cookies.remove).toHaveBeenCalledWith("wallet-auth", {
secure: true,
Expand All @@ -119,5 +159,8 @@ describe("auth.utils", () => {
expect(safeLocalStorage.removeItem).toHaveBeenCalledWith(
"6529-wallet-role"
);
expect(safeLocalStorage.removeItem).toHaveBeenCalledWith(
"auth-role-addr"
);
});
});
40 changes: 22 additions & 18 deletions services/auth/auth.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const getJwtExpiration = (jwt: string): number => {
return decodedJwt.exp;
};

const getAddressRoleStorageKey = (address: string): string => {
return `auth-role-${address.toLowerCase()}`;
};

// TODO: remove these cookies once migration is complete
export const migrateCookiesToLocalStorage = () => {
const walletAddress = Cookies.get(WALLET_ADDRESS_COOKIE);
Expand Down Expand Up @@ -69,8 +73,13 @@ export const setAuthJwt = (

safeLocalStorage.setItem(WALLET_ADDRESS_STORAGE_KEY, address);
safeLocalStorage.setItem(WALLET_REFRESH_TOKEN_STORAGE_KEY, refreshToken);
const addressRoleStorageKey = getAddressRoleStorageKey(address);
if (role) {
safeLocalStorage.setItem(WALLET_ROLE_STORAGE_KEY, role);
safeLocalStorage.setItem(addressRoleStorageKey, role);
} else {
safeLocalStorage.removeItem(WALLET_ROLE_STORAGE_KEY);
safeLocalStorage.removeItem(addressRoleStorageKey);
}
};

Expand Down Expand Up @@ -101,36 +110,31 @@ export const getWalletRole = () => {
};

export const removeAuthJwt = () => {
const storedAddress = safeLocalStorage.getItem(WALLET_ADDRESS_STORAGE_KEY);
Cookies.remove(WALLET_AUTH_COOKIE, COOKIE_OPTIONS);
safeLocalStorage.removeItem(WALLET_ADDRESS_STORAGE_KEY);
safeLocalStorage.removeItem(WALLET_REFRESH_TOKEN_STORAGE_KEY);
safeLocalStorage.removeItem(WALLET_ROLE_STORAGE_KEY);
if (storedAddress) {
safeLocalStorage.removeItem(getAddressRoleStorageKey(storedAddress));
}
};

/**
* Validates JWT role against wallet role with fail-fast security checks
*
* @param freshJwt - The fresh JWT token from server response
* @param walletRole - The role associated with the current wallet
* @param requestedRole - The role that was requested (optional)
* @returns The validated role from the fresh JWT
* @throws Error if validation fails
* Synchronizes wallet role storage with the authoritative value from the server.
*/
export const syncWalletRoleWithServer = (
serverRole: string | null,
address: string
): void => {
const currentRole = getWalletRole();
if (currentRole !== serverRole) {
// Update local storage to match server
if (serverRole) {
safeLocalStorage.setItem(
`auth-role-${address.toLowerCase()}`,
serverRole
);
} else {
safeLocalStorage.removeItem(`auth-role-${address.toLowerCase()}`);
}
const addressRoleStorageKey = getAddressRoleStorageKey(address);

if (serverRole) {
safeLocalStorage.setItem(WALLET_ROLE_STORAGE_KEY, serverRole);
safeLocalStorage.setItem(addressRoleStorageKey, serverRole);
} else {
safeLocalStorage.removeItem(WALLET_ROLE_STORAGE_KEY);
safeLocalStorage.removeItem(addressRoleStorageKey);
}
};

Expand Down