Compare commits

...

1 Commits

Author SHA1 Message Date
Guido D'Orsi
42212bfe6e fix: fixes invalid authentication state when logging out after signUp 2025-02-28 19:51:22 +01:00
7 changed files with 102 additions and 31 deletions

View File

@@ -0,0 +1,6 @@
---
"jazz-react-core": patch
"jazz-tools": patch
---
Fixes invalid authentication state when logging out after signUp

View File

@@ -1,9 +1,10 @@
// @vitest-environment happy-dom
import { mnemonicToEntropy } from "@scure/bip39";
import { AuthSecretStorage, KvStoreContext } from "jazz-tools";
import { Account, AuthSecretStorage, KvStoreContext } from "jazz-tools";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { usePassphraseAuth } from "../auth/PassphraseAuth";
import { createUseAccountHooks } from "../hooks";
import {
createJazzTestAccount,
createJazzTestGuest,
@@ -12,6 +13,8 @@ import {
import { testWordlist } from "./fixtures.js";
import { act, renderHook, waitFor } from "./testUtils";
const { useAccount } = createUseAccountHooks<Account>();
describe("usePassphraseAuth", () => {
beforeEach(async () => {
await setupJazzTestSync();
@@ -105,4 +108,73 @@ describe("usePassphraseAuth", () => {
expect(await result.current.signUp()).toBe(passphrase);
});
it("should be able to logout after sign up", async () => {
const account = await createJazzTestAccount({});
const accounts: string[] = [];
const updates: { state: string; accountIndex: number }[] = [];
const { result } = renderHook(
() => {
const passphraseAuth = usePassphraseAuth({ wordlist: testWordlist });
const account = useAccount();
if (!accounts.includes(account.me.id)) {
accounts.push(account.me.id);
}
updates.push({
state: passphraseAuth.state,
accountIndex: accounts.indexOf(account.me.id),
});
return { passphraseAuth, account };
},
{
account,
isAuthenticated: false,
},
);
expect(result.current?.passphraseAuth.state).toBe("anonymous");
expect(result.current?.account?.me).toBeDefined();
const id = result.current?.account?.me?.id;
await act(async () => {
await result.current?.passphraseAuth.signUp();
});
expect(result.current?.passphraseAuth.state).toBe("signedIn");
expect(result.current?.account?.me.id).toBe(id);
await act(async () => {
await result.current?.account?.logOut();
});
expect(result.current?.passphraseAuth.state).toBe("anonymous");
expect(result.current?.account?.me.id).not.toBe(id);
expect(updates).toMatchInlineSnapshot(`
[
{
"accountIndex": 0,
"state": "anonymous",
},
{
"accountIndex": 0,
"state": "anonymous",
},
{
"accountIndex": 0,
"state": "signedIn",
},
{
"accountIndex": 1,
"state": "anonymous",
},
]
`);
});
});

View File

@@ -16,7 +16,6 @@ export type AuthSetPayload = {
export class AuthSecretStorage {
private listeners: Set<(isAuthenticated: boolean) => void>;
public isAuthenticated: boolean;
notify = false;
constructor() {
this.listeners = new Set();
@@ -97,7 +96,7 @@ export class AuthSecretStorage {
};
}
async set(payload: AuthSetPayload) {
async setWithoutNotify(payload: AuthSetPayload) {
const kvStore = KvStoreContext.getInstance().getStorage();
await kvStore.set(
STORAGE_KEY,
@@ -110,10 +109,11 @@ export class AuthSecretStorage {
provider: payload.provider,
}),
);
}
if (this.notify) {
this.emitUpdate(payload);
}
async set(payload: AuthSetPayload) {
this.setWithoutNotify(payload);
this.emitUpdate(payload);
}
getIsAuthenticated(data: AuthCredentials | null): boolean {
@@ -139,12 +139,13 @@ export class AuthSecretStorage {
}
}
async clear() {
async clearWithoutNotify() {
const kvStore = KvStoreContext.getInstance().getStorage();
await kvStore.delete(STORAGE_KEY);
}
if (this.notify) {
this.emitUpdate(null);
}
async clear() {
await this.clearWithoutNotify();
this.emitUpdate(null);
}
}

View File

@@ -43,10 +43,6 @@ export class JazzContextManager<
protected context: PlatformSpecificContext<Acc> | undefined;
protected props: P | undefined;
protected authSecretStorage = new AuthSecretStorage();
protected authSecretStorageWithNotify = Object.assign(
Object.create(this.authSecretStorage),
{ notify: true },
);
protected authenticating = false;
constructor() {
@@ -103,9 +99,7 @@ export class JazzContextManager<
}
getAuthSecretStorage() {
// External updates of the auth secret storage are notified by default (e.g. when registering a new Auth provider)
// We skip internal notify to ensure that the isAuthenticated changes are notified along with the context updates
return this.authSecretStorageWithNotify;
return this.authSecretStorage;
}
logOut = async () => {

View File

@@ -219,7 +219,7 @@ export async function createJazzContext<Acc extends Account>(options: {
AccountSchema: options.AccountSchema,
sessionProvider: options.sessionProvider,
onLogOut: () => {
authSecretStorage.clear();
authSecretStorage.clearWithoutNotify();
},
});
} else {
@@ -240,12 +240,12 @@ export async function createJazzContext<Acc extends Account>(options: {
crypto,
AccountSchema: options.AccountSchema,
onLogOut: async () => {
await authSecretStorage.clear();
await authSecretStorage.clearWithoutNotify();
},
});
if (!options.newAccountProps) {
await authSecretStorage.set({
await authSecretStorage.setWithoutNotify({
accountID: context.account.id,
secretSeed,
accountSecret: context.node.account.agentSecret,

View File

@@ -257,11 +257,10 @@ describe("AuthSecretStorage", () => {
});
});
describe("notify=true", () => {
describe("notify", () => {
beforeEach(() => {
kvStore.clearAll();
authSecretStorage = new AuthSecretStorage();
authSecretStorage.notify = true;
});
describe("set", () => {
@@ -337,7 +336,7 @@ describe("AuthSecretStorage", () => {
});
});
describe("notify=false", () => {
describe("without notify", () => {
beforeEach(() => {
kvStore.clearAll();
authSecretStorage = new AuthSecretStorage();
@@ -348,7 +347,7 @@ describe("AuthSecretStorage", () => {
const handler = vi.fn();
authSecretStorage.onUpdate(handler);
await authSecretStorage.set({
await authSecretStorage.setWithoutNotify({
accountID: "test123" as ID<Account>,
accountSecret:
"secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
@@ -365,7 +364,7 @@ describe("AuthSecretStorage", () => {
});
it("should return false for anonymous credentials", async () => {
await authSecretStorage.set({
await authSecretStorage.setWithoutNotify({
accountID: "test123" as ID<Account>,
accountSecret:
"secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
@@ -376,7 +375,7 @@ describe("AuthSecretStorage", () => {
});
it("should return true for non-anonymous credentials", async () => {
await authSecretStorage.set({
await authSecretStorage.setWithoutNotify({
accountID: "test123" as ID<Account>,
accountSecret:
"secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
@@ -395,7 +394,7 @@ describe("AuthSecretStorage", () => {
});
it("should return true when the provider is missing", async () => {
await authSecretStorage.set({
await authSecretStorage.setWithoutNotify({
accountID: "test123" as ID<Account>,
accountSecret:
"secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
@@ -414,7 +413,7 @@ describe("AuthSecretStorage", () => {
describe("clear", () => {
it("should not emit update event when clearing", async () => {
await authSecretStorage.set({
await authSecretStorage.setWithoutNotify({
accountID: "test123" as ID<Account>,
accountSecret:
"secret123" as `sealerSecret_z${string}/signerSecret_z${string}`,
@@ -424,7 +423,7 @@ describe("AuthSecretStorage", () => {
const handler = vi.fn();
authSecretStorage.onUpdate(handler);
await authSecretStorage.clear();
await authSecretStorage.clearWithoutNotify();
expect(handler).not.toHaveBeenCalled();
});

View File

@@ -1,8 +1,7 @@
// @vitest-environment happy-dom
import { mnemonicToEntropy } from "@scure/bip39";
import { AgentSecret } from "cojson";
import { PureJSCrypto } from "cojson/src/crypto/PureJSCrypto";
import { PureJSCrypto } from "cojson/crypto/PureJSCrypto";
import {
Account,
AuthSecretStorage,