fix(auth): prevent credential resurrection after Anthropic logout
The logout flow had two bugs causing credentials to reappear immediately: 1. The codebase has two separate auth storage Proxy chains: - createFusionAuthStorage (engine, for agents) - mergeAuthStorageReads (CLI, for dashboard UI) Neither had a logout trap, so supplemental credentials from ~/.claude/.credentials.json were never excluded after logout. 2. The upstream AuthStorage.hasAuth() checks environment variables (ANTHROPIC_API_KEY), which always returns true regardless of logout. Fix: Add loggedOutProviders tracking to both Proxy chains. All query traps (has, hasAuth, get, getAll, list, getApiKey) return false/undefined for logged-out providers instead of delegating to the underlying storage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -209,4 +209,137 @@ describe("wrapAuthStorageWithApiKeyProviders", () => {
|
|||||||
expect(wrapped.hasApiKey("anthropic")).toBe(true);
|
expect(wrapped.hasApiKey("anthropic")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("logout with fallback credentials", () => {
|
||||||
|
it("hides fallback credentials after logout", () => {
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
|
||||||
|
// Before logout, fallback credentials are visible
|
||||||
|
expect(merged.has("anthropic")).toBe(true);
|
||||||
|
expect(merged.hasAuth("anthropic")).toBe(true);
|
||||||
|
expect(merged.get("anthropic")).toEqual({ type: "api_key", key: "claude-access-token" });
|
||||||
|
|
||||||
|
// Log out
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
// After logout, fallback credentials are hidden
|
||||||
|
expect(merged.has("anthropic")).toBe(false);
|
||||||
|
expect(merged.hasAuth("anthropic")).toBe(false);
|
||||||
|
expect(merged.get("anthropic")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resurrect fallback credentials on reload after logout", () => {
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
// reload() should NOT bring back the fallback credential
|
||||||
|
merged.reload();
|
||||||
|
|
||||||
|
expect(merged.has("anthropic")).toBe(false);
|
||||||
|
expect(merged.hasAuth("anthropic")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes logged-out providers from getAll()", () => {
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
openrouter: { type: "api_key", key: "openrouter-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
const all = merged.getAll();
|
||||||
|
expect("anthropic" in all).toBe(false);
|
||||||
|
expect("openrouter" in all).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes logged-out providers from list()", () => {
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
openrouter: { type: "api_key", key: "openrouter-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
expect(merged.list()).not.toContain("anthropic");
|
||||||
|
expect(merged.list()).toContain("openrouter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides fallback getApiKey after logout", async () => {
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
|
||||||
|
expect(await merged.getApiKey("anthropic")).toBe("claude-access-token");
|
||||||
|
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
expect(await merged.getApiKey("anthropic")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-enables fallback credentials after re-authentication via set()", () => {
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
// Re-authenticate
|
||||||
|
merged.set("anthropic", { type: "api_key", key: "new-key" });
|
||||||
|
|
||||||
|
// Provider is visible again (from primary storage)
|
||||||
|
expect(merged.has("anthropic")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only hides the logged-out provider, not other fallback providers", () => {
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
openrouter: { type: "api_key", key: "openrouter-key" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
// anthropic is hidden
|
||||||
|
expect(merged.hasAuth("anthropic")).toBe(false);
|
||||||
|
// openrouter is still visible
|
||||||
|
expect(merged.hasAuth("openrouter")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for hasAuth even when underlying storage reports auth via env var", () => {
|
||||||
|
// Simulate the real AuthStorage which checks env vars in hasAuth
|
||||||
|
const fusionAuth = makeAuthStorage();
|
||||||
|
fusionAuth.hasAuth = vi.fn(() => true); // env var would make this true
|
||||||
|
const fallbackAuth = makeAuthStorage({
|
||||||
|
anthropic: { type: "api_key", key: "claude-access-token" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const merged = mergeAuthStorageReads(fusionAuth, [fallbackAuth]);
|
||||||
|
merged.logout("anthropic");
|
||||||
|
|
||||||
|
// Even though the underlying storage reports hasAuth=true (env var),
|
||||||
|
// the logged-out provider must still return false
|
||||||
|
expect(merged.hasAuth("anthropic")).toBe(false);
|
||||||
|
expect(merged.has("anthropic")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -138,6 +138,12 @@ export function mergeAuthStorageReads(
|
|||||||
readFallbackAuthStorages: ReadFallbackAuthStorage[] = [],
|
readFallbackAuthStorages: ReadFallbackAuthStorage[] = [],
|
||||||
): AuthStorage {
|
): AuthStorage {
|
||||||
const readAuthStorages = [authStorage, ...readFallbackAuthStorages];
|
const readAuthStorages = [authStorage, ...readFallbackAuthStorages];
|
||||||
|
|
||||||
|
// Providers the user has explicitly logged out from. These should not be
|
||||||
|
// "resurrected" from supplemental credential files (e.g. ~/.claude/.credentials.json).
|
||||||
|
// Cleared when the user re-authenticates via set().
|
||||||
|
const loggedOutProviders = new Set<string>();
|
||||||
|
|
||||||
const selectCredential = (
|
const selectCredential = (
|
||||||
providerId: string,
|
providerId: string,
|
||||||
storages: Array<Pick<ReadFallbackAuthStorage, "get">>,
|
storages: Array<Pick<ReadFallbackAuthStorage, "get">>,
|
||||||
@@ -149,11 +155,19 @@ export function mergeAuthStorageReads(
|
|||||||
return best;
|
return best;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCredential = (providerId: string) => selectCredential(providerId, readAuthStorages);
|
const getCredential = (providerId: string) => {
|
||||||
|
if (loggedOutProviders.has(providerId)) {
|
||||||
|
return authStorage.get(providerId) as StoredCredential | undefined;
|
||||||
|
}
|
||||||
|
return selectCredential(providerId, readAuthStorages);
|
||||||
|
};
|
||||||
|
|
||||||
const syncFallbackOauthCredentials = () => {
|
const syncFallbackOauthCredentials = () => {
|
||||||
const providerIds = new Set(readFallbackAuthStorages.flatMap((storage) => storage.list()));
|
const providerIds = new Set(readFallbackAuthStorages.flatMap((storage) => storage.list()));
|
||||||
for (const providerId of providerIds) {
|
for (const providerId of providerIds) {
|
||||||
|
if (loggedOutProviders.has(providerId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const current = authStorage.get(providerId) as StoredCredential | undefined;
|
const current = authStorage.get(providerId) as StoredCredential | undefined;
|
||||||
const candidate = selectCredential(providerId, readFallbackAuthStorages);
|
const candidate = selectCredential(providerId, readFallbackAuthStorages);
|
||||||
if (!shouldHydrateStoredCredential(current, candidate)) {
|
if (!shouldHydrateStoredCredential(current, candidate)) {
|
||||||
@@ -169,6 +183,20 @@ export function mergeAuthStorageReads(
|
|||||||
|
|
||||||
return new Proxy(authStorage, {
|
return new Proxy(authStorage, {
|
||||||
get(target, prop, receiver) {
|
get(target, prop, receiver) {
|
||||||
|
if (prop === "logout") {
|
||||||
|
return (provider: string) => {
|
||||||
|
loggedOutProviders.add(provider);
|
||||||
|
target.logout(provider);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === "set") {
|
||||||
|
return (provider: string, credential: AuthCredential) => {
|
||||||
|
loggedOutProviders.delete(provider);
|
||||||
|
target.set(provider, credential);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (prop === "reload") {
|
if (prop === "reload") {
|
||||||
return () => {
|
return () => {
|
||||||
for (const storage of readAuthStorages) {
|
for (const storage of readAuthStorages) {
|
||||||
@@ -183,11 +211,21 @@ export function mergeAuthStorageReads(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "has") {
|
if (prop === "has") {
|
||||||
return (provider: string) => readAuthStorages.some((storage) => Boolean(storage.get(provider)));
|
return (provider: string) => {
|
||||||
|
if (loggedOutProviders.has(provider)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return readAuthStorages.some((storage) => Boolean(storage.get(provider)));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "hasAuth") {
|
if (prop === "hasAuth") {
|
||||||
return (provider: string) => readAuthStorages.some((storage) => storage.hasAuth(provider));
|
return (provider: string) => {
|
||||||
|
if (loggedOutProviders.has(provider)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return readAuthStorages.some((storage) => storage.hasAuth(provider));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "getAll") {
|
if (prop === "getAll") {
|
||||||
@@ -195,6 +233,9 @@ export function mergeAuthStorageReads(
|
|||||||
const providerIds = new Set(readAuthStorages.flatMap((storage) => storage.list()));
|
const providerIds = new Set(readAuthStorages.flatMap((storage) => storage.list()));
|
||||||
const merged: Record<string, StoredCredential> = {};
|
const merged: Record<string, StoredCredential> = {};
|
||||||
for (const providerId of providerIds) {
|
for (const providerId of providerIds) {
|
||||||
|
if (loggedOutProviders.has(providerId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const credential = getCredential(providerId);
|
const credential = getCredential(providerId);
|
||||||
if (credential) {
|
if (credential) {
|
||||||
merged[providerId] = credential;
|
merged[providerId] = credential;
|
||||||
@@ -205,11 +246,17 @@ export function mergeAuthStorageReads(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "list") {
|
if (prop === "list") {
|
||||||
return () => Array.from(new Set(readAuthStorages.flatMap((storage) => storage.list())));
|
return () => {
|
||||||
|
const providers = readAuthStorages.flatMap((storage) => storage.list());
|
||||||
|
return Array.from(new Set(providers.filter((p) => !loggedOutProviders.has(p))));
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "getApiKey") {
|
if (prop === "getApiKey") {
|
||||||
return async (providerId: string) => {
|
return async (providerId: string) => {
|
||||||
|
if (loggedOutProviders.has(providerId)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
for (const storage of readAuthStorages) {
|
for (const storage of readAuthStorages) {
|
||||||
const apiKey = await storage.getApiKey(providerId);
|
const apiKey = await storage.getApiKey(providerId);
|
||||||
if (apiKey) return apiKey;
|
if (apiKey) return apiKey;
|
||||||
|
|||||||
@@ -899,6 +899,7 @@ export const registerAuthRoutes: ApiRouteRegistrar = (ctx) => {
|
|||||||
|
|
||||||
const storage = getAuthStorage();
|
const storage = getAuthStorage();
|
||||||
storage.logout(provider);
|
storage.logout(provider);
|
||||||
|
clearUsageCache();
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
if (err instanceof ApiError) {
|
if (err instanceof ApiError) {
|
||||||
|
|||||||
@@ -396,4 +396,161 @@ describe("createFusionAuthStorage", () => {
|
|||||||
expect(authStorage.hasAuth("dynamic-provider")).toBe(true);
|
expect(authStorage.hasAuth("dynamic-provider")).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("logout with supplemental credentials", () => {
|
||||||
|
it("hides supplemental Claude credentials after logout", async () => {
|
||||||
|
const claudeDir = join(homeDir, ".claude");
|
||||||
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "claude-access-token",
|
||||||
|
refreshToken: "claude-refresh-token",
|
||||||
|
expiresAt: Date.now() + 3_600_000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const authStorage = createFusionAuthStorage();
|
||||||
|
|
||||||
|
// Before logout, supplemental credentials are visible
|
||||||
|
expect(authStorage.has("anthropic")).toBe(true);
|
||||||
|
expect(authStorage.hasAuth("anthropic")).toBe(true);
|
||||||
|
expect(await authStorage.getApiKey("anthropic")).toBe("claude-access-token");
|
||||||
|
|
||||||
|
// Log out
|
||||||
|
authStorage.logout("anthropic");
|
||||||
|
|
||||||
|
// After logout, supplemental credentials are hidden
|
||||||
|
expect(authStorage.has("anthropic")).toBe(false);
|
||||||
|
expect(authStorage.hasAuth("anthropic")).toBe(false);
|
||||||
|
expect(authStorage.get("anthropic")).toBeUndefined();
|
||||||
|
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not resurrect supplemental credentials on reload after logout", async () => {
|
||||||
|
const claudeDir = join(homeDir, ".claude");
|
||||||
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "claude-access-token",
|
||||||
|
refreshToken: "claude-refresh-token",
|
||||||
|
expiresAt: Date.now() + 3_600_000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const authStorage = createFusionAuthStorage();
|
||||||
|
authStorage.logout("anthropic");
|
||||||
|
|
||||||
|
// reload() should NOT bring back the supplemental credential
|
||||||
|
authStorage.reload();
|
||||||
|
|
||||||
|
expect(authStorage.has("anthropic")).toBe(false);
|
||||||
|
expect(authStorage.hasAuth("anthropic")).toBe(false);
|
||||||
|
expect(await authStorage.getApiKey("anthropic")).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes logged-out providers from getAll()", async () => {
|
||||||
|
const claudeDir = join(homeDir, ".claude");
|
||||||
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "claude-access-token",
|
||||||
|
refreshToken: "claude-refresh-token",
|
||||||
|
expiresAt: Date.now() + 3_600_000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const authStorage = createFusionAuthStorage();
|
||||||
|
authStorage.logout("anthropic");
|
||||||
|
|
||||||
|
const all = authStorage.getAll();
|
||||||
|
expect("anthropic" in all).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("excludes logged-out providers from list()", async () => {
|
||||||
|
const claudeDir = join(homeDir, ".claude");
|
||||||
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "claude-access-token",
|
||||||
|
refreshToken: "claude-refresh-token",
|
||||||
|
expiresAt: Date.now() + 3_600_000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const authStorage = createFusionAuthStorage();
|
||||||
|
authStorage.logout("anthropic");
|
||||||
|
|
||||||
|
expect(authStorage.list()).not.toContain("anthropic");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("re-enables supplemental credentials after re-authentication via set()", async () => {
|
||||||
|
const claudeDir = join(homeDir, ".claude");
|
||||||
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
writeFileSync(
|
||||||
|
join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "claude-access-token",
|
||||||
|
refreshToken: "claude-refresh-token",
|
||||||
|
expiresAt: Date.now() + 3_600_000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const authStorage = createFusionAuthStorage();
|
||||||
|
authStorage.logout("anthropic");
|
||||||
|
|
||||||
|
// Re-authenticate
|
||||||
|
authStorage.set("anthropic", { type: "api_key", key: "new-key" });
|
||||||
|
|
||||||
|
// Provider is visible again
|
||||||
|
expect(authStorage.has("anthropic")).toBe(true);
|
||||||
|
expect(await authStorage.getApiKey("anthropic")).toBe("new-key");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("only hides the logged-out provider, not other supplemental providers", async () => {
|
||||||
|
const claudeDir = join(homeDir, ".claude");
|
||||||
|
const legacyDir = join(homeDir, ".pi", "agent");
|
||||||
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
mkdirSync(legacyDir, { recursive: true });
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "claude-access-token",
|
||||||
|
refreshToken: "claude-refresh-token",
|
||||||
|
expiresAt: Date.now() + 3_600_000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
writeFileSync(
|
||||||
|
join(legacyDir, "auth.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
openrouter: { type: "api_key", key: "legacy-openrouter-key" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const authStorage = createFusionAuthStorage();
|
||||||
|
authStorage.logout("anthropic");
|
||||||
|
|
||||||
|
// anthropic is hidden
|
||||||
|
expect(authStorage.hasAuth("anthropic")).toBe(false);
|
||||||
|
// openrouter is still visible
|
||||||
|
expect(authStorage.hasAuth("openrouter")).toBe(true);
|
||||||
|
expect(await authStorage.getApiKey("openrouter")).toBe("legacy-openrouter-key");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -143,8 +143,16 @@ export function createFusionAuthStorage(): AuthStorage {
|
|||||||
// models.json provider API keys — final fallback after primary auth and supplemental auth.json files
|
// models.json provider API keys — final fallback after primary auth and supplemental auth.json files
|
||||||
let modelsJsonApiKeys = readModelsJsonApiKeys();
|
let modelsJsonApiKeys = readModelsJsonApiKeys();
|
||||||
|
|
||||||
|
// Providers the user has explicitly logged out from. These should not be
|
||||||
|
// "resurrected" from supplemental credential files (e.g. ~/.claude/.credentials.json).
|
||||||
|
// Cleared when the user re-authenticates via set().
|
||||||
|
const loggedOutProviders = new Set<string>();
|
||||||
|
|
||||||
const syncSupplementalOauthCredentials = () => {
|
const syncSupplementalOauthCredentials = () => {
|
||||||
for (const [provider, credential] of Object.entries(supplementalCredentials)) {
|
for (const [provider, credential] of Object.entries(supplementalCredentials)) {
|
||||||
|
if (loggedOutProviders.has(provider)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const current = primary.get(provider) as StoredCredential | undefined;
|
const current = primary.get(provider) as StoredCredential | undefined;
|
||||||
if (!shouldHydrateStoredCredential(current, credential)) {
|
if (!shouldHydrateStoredCredential(current, credential)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -168,6 +176,20 @@ export function createFusionAuthStorage(): AuthStorage {
|
|||||||
},
|
},
|
||||||
|
|
||||||
get(target, prop, receiver) {
|
get(target, prop, receiver) {
|
||||||
|
if (prop === "logout") {
|
||||||
|
return (provider: string) => {
|
||||||
|
loggedOutProviders.add(provider);
|
||||||
|
target.logout(provider);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prop === "set") {
|
||||||
|
return (provider: string, credential: AuthCredential) => {
|
||||||
|
loggedOutProviders.delete(provider);
|
||||||
|
target.set(provider, credential);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (prop === "reload") {
|
if (prop === "reload") {
|
||||||
return () => {
|
return () => {
|
||||||
target.reload();
|
target.reload();
|
||||||
@@ -178,32 +200,48 @@ export function createFusionAuthStorage(): AuthStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "get") {
|
if (prop === "get") {
|
||||||
return (provider: string) =>
|
return (provider: string) => {
|
||||||
choosePreferredStoredCredential(
|
if (loggedOutProviders.has(provider)) {
|
||||||
|
return target.get(provider);
|
||||||
|
}
|
||||||
|
return choosePreferredStoredCredential(
|
||||||
target.get(provider) as StoredCredential | undefined,
|
target.get(provider) as StoredCredential | undefined,
|
||||||
supplementalCredentials[provider],
|
supplementalCredentials[provider],
|
||||||
);
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "has") {
|
if (prop === "has") {
|
||||||
return (provider: string) => target.has(provider) || provider in supplementalCredentials || modelsJsonApiKeys.has(provider);
|
return (provider: string) => {
|
||||||
|
if (loggedOutProviders.has(provider)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return target.has(provider) || provider in supplementalCredentials || modelsJsonApiKeys.has(provider);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "hasAuth") {
|
if (prop === "hasAuth") {
|
||||||
return (provider: string) => target.hasAuth(provider) || Boolean(supplementalCredentials[provider]) || modelsJsonApiKeys.has(provider);
|
return (provider: string) => {
|
||||||
|
if (loggedOutProviders.has(provider)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return target.hasAuth(provider) || Boolean(supplementalCredentials[provider]) || modelsJsonApiKeys.has(provider);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "getAll") {
|
if (prop === "getAll") {
|
||||||
return () => {
|
return () => {
|
||||||
const providerIds = new Set([
|
const providerIds = new Set([
|
||||||
...Object.keys(supplementalCredentials),
|
|
||||||
...Object.keys(target.getAll() as Record<string, StoredCredential>),
|
...Object.keys(target.getAll() as Record<string, StoredCredential>),
|
||||||
|
...(loggedOutProviders.size > 0
|
||||||
|
? Object.keys(supplementalCredentials).filter((p) => !loggedOutProviders.has(p))
|
||||||
|
: Object.keys(supplementalCredentials)),
|
||||||
]);
|
]);
|
||||||
const merged: Record<string, StoredCredential> = {};
|
const merged: Record<string, StoredCredential> = {};
|
||||||
for (const providerId of providerIds) {
|
for (const providerId of providerIds) {
|
||||||
const credential = choosePreferredStoredCredential(
|
const credential = choosePreferredStoredCredential(
|
||||||
(target.get(providerId) as StoredCredential | undefined),
|
(target.get(providerId) as StoredCredential | undefined),
|
||||||
supplementalCredentials[providerId],
|
loggedOutProviders.has(providerId) ? undefined : supplementalCredentials[providerId],
|
||||||
);
|
);
|
||||||
if (credential) {
|
if (credential) {
|
||||||
merged[providerId] = credential;
|
merged[providerId] = credential;
|
||||||
@@ -214,11 +252,23 @@ export function createFusionAuthStorage(): AuthStorage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "list") {
|
if (prop === "list") {
|
||||||
return () => Array.from(new Set([...Object.keys(supplementalCredentials), ...target.list(), ...modelsJsonApiKeys.keys()]));
|
return () => {
|
||||||
|
const providers = new Set([...target.list(), ...modelsJsonApiKeys.keys()]);
|
||||||
|
for (const p of Object.keys(supplementalCredentials)) {
|
||||||
|
if (!loggedOutProviders.has(p)) {
|
||||||
|
providers.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(providers);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prop === "getApiKey") {
|
if (prop === "getApiKey") {
|
||||||
return async (provider: string) => {
|
return async (provider: string) => {
|
||||||
|
if (loggedOutProviders.has(provider)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Primary Fusion auth
|
// 1. Primary Fusion auth
|
||||||
const primaryKey = await target.getApiKey(provider);
|
const primaryKey = await target.getApiKey(provider);
|
||||||
if (primaryKey) return primaryKey;
|
if (primaryKey) return primaryKey;
|
||||||
|
|||||||
Reference in New Issue
Block a user