fix: 修复 OIDC 用户预配与跨标签页登录态

增加受控 JIT 本地用户投影,并使用 HttpOnly Cookie 在新标签页恢复认证中心登录态。补充错误语义、安全校验、自动化测试与回滚配置。
This commit is contained in:
2026-07-13 17:07:52 +08:00
parent 17b1f77e1d
commit a81a7b5200
37 changed files with 2694 additions and 179 deletions
@@ -0,0 +1,47 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { OIDC_BROWSER_SESSION_CREDENTIAL } from '../api';
import { activateOIDCBrowserSession, restoreOIDCBrowserSession } from './oidc-browser-session';
class MemoryStorage {
private values = new Map<string, string>();
getItem(key: string) { return this.values.get(key) ?? null; }
setItem(key: string, value: string) { this.values.set(key, value); }
removeItem(key: string) { this.values.delete(key); }
}
describe('OIDC browser session lifecycle', () => {
beforeEach(() => {
vi.stubGlobal('window', { localStorage: new MemoryStorage(), sessionStorage: new MemoryStorage() });
});
it('moves an OIDC access token into the HttpOnly session and clears browser token storage', async () => {
window.localStorage.setItem('easyai_ai_gateway_access_token', 'old-local-token');
window.sessionStorage.setItem('easyai_ai_gateway_oidc_access_token', 'old-oidc-token');
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 204 })));
const credential = await activateOIDCBrowserSession('new-oidc-token');
expect(credential).toBe(OIDC_BROWSER_SESSION_CREDENTIAL);
expect(window.localStorage.getItem('easyai_ai_gateway_access_token')).toBeNull();
expect(window.sessionStorage.getItem('easyai_ai_gateway_oidc_access_token')).toBeNull();
});
it('restores a shared cookie session in a fresh tab', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
sub: 'platform-user', source: 'oidc', gatewayUserId: 'gateway-user',
}), { status: 200, headers: { 'Content-Type': 'application/json' } })));
const restored = await restoreOIDCBrowserSession();
expect(restored?.credential).toBe(OIDC_BROWSER_SESSION_CREDENTIAL);
expect(restored?.user.sub).toBe('platform-user');
});
it('treats a missing or expired shared cookie as a signed-out tab', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(JSON.stringify({
error: { message: 'unauthorized', status: 401 },
}), { status: 401, headers: { 'Content-Type': 'application/json' } })));
await expect(restoreOIDCBrowserSession()).resolves.toBeNull();
});
});