From efa08494259612c4e9287eba2ff370c13885222e Mon Sep 17 00:00:00 2001 From: Jakob Karlstrand Date: Fri, 11 Sep 2026 13:59:17 +0200 Subject: [PATCH 1/2] fix(auth): point OIDC issuer at auth.berget.ai after Keycloak cutover Follow-up to incident 2026-09-10-0: token issuance moved from keycloak.berget.ai to auth.berget.ai. The CLI still discovered and refreshed against the legacy hostname, which breaks PKCE login (strict-iss validation since the auth-server 301:es legacy requests) and token refresh (Keycloak 26.6 requires mint-host == refresh-host, invalid_grant on every existing session). Flip prod to auth.berget.ai and stage/local to auth.stage.berget.ai. --- src/auth/__tests__/config.test.ts | 8 ++++---- src/auth/config.ts | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/auth/__tests__/config.test.ts b/src/auth/__tests__/config.test.ts index 59ee033..8ee6fdd 100644 --- a/src/auth/__tests__/config.test.ts +++ b/src/auth/__tests__/config.test.ts @@ -6,7 +6,7 @@ describe('getAuthConfig', () => { it('returns production URLs by default', () => { const config = getAuthConfig(); expect(config.apiBaseUrl).toBe('https://api.berget.ai'); - expect(config.keycloakUrl).toBe('https://keycloak.berget.ai'); + expect(config.keycloakUrl).toBe('https://auth.berget.ai'); expect(config.realm).toBe('berget'); expect(config.clientId).toBe('berget-code'); }); @@ -14,20 +14,20 @@ describe('getAuthConfig', () => { it('returns stage URLs when stage: true', () => { const config = getAuthConfig({ stage: true }); expect(config.apiBaseUrl).toBe('https://api.stage.berget.ai'); - expect(config.keycloakUrl).toBe('https://keycloak.stage.berget.ai'); + expect(config.keycloakUrl).toBe('https://auth.stage.berget.ai'); }); it('returns local URLs when local: true', () => { const config = getAuthConfig({ local: true }); expect(config.apiBaseUrl).toBe('http://localhost:3000'); - expect(config.keycloakUrl).toBe('https://keycloak.stage.berget.ai'); + expect(config.keycloakUrl).toBe('https://auth.stage.berget.ai'); }); it('overrides apiBaseUrl with BERGET_API_URL env var', () => { vi.stubEnv('BERGET_API_URL', 'https://custom.api.example.com'); const config = getAuthConfig(); expect(config.apiBaseUrl).toBe('https://custom.api.example.com'); - expect(config.keycloakUrl).toBe('https://keycloak.berget.ai'); + expect(config.keycloakUrl).toBe('https://auth.berget.ai'); vi.unstubAllEnvs(); }); diff --git a/src/auth/config.ts b/src/auth/config.ts index dfd22da..3a90bc6 100644 --- a/src/auth/config.ts +++ b/src/auth/config.ts @@ -20,19 +20,19 @@ export function getAuthConfig(options?: { local?: boolean; stage?: boolean }): A apiBaseUrl = process.env.BERGET_API_URL; // Infer keycloak from API URL for custom endpoints if (apiBaseUrl.includes('localhost') || apiBaseUrl.includes('stage.')) { - keycloakUrl = 'https://keycloak.stage.berget.ai'; + keycloakUrl = 'https://auth.stage.berget.ai'; } else { - keycloakUrl = 'https://keycloak.berget.ai'; + keycloakUrl = 'https://auth.berget.ai'; } } else if (options?.local) { apiBaseUrl = 'http://localhost:3000'; - keycloakUrl = 'https://keycloak.stage.berget.ai'; + keycloakUrl = 'https://auth.stage.berget.ai'; } else if (options?.stage) { apiBaseUrl = 'https://api.stage.berget.ai'; - keycloakUrl = 'https://keycloak.stage.berget.ai'; + keycloakUrl = 'https://auth.stage.berget.ai'; } else { apiBaseUrl = 'https://api.berget.ai'; - keycloakUrl = 'https://keycloak.berget.ai'; + keycloakUrl = 'https://auth.berget.ai'; } return { From d002083a9b73071f8876aeab45c422b6f0f6b928 Mon Sep 17 00:00:00 2001 From: Jakob Karlstrand Date: Fri, 11 Sep 2026 14:03:25 +0200 Subject: [PATCH 2/2] test(auth): update legacy keycloak.* fixtures to auth.* issuers Hygiene after the issuer migration: make the auth test fixtures mirror the real prod/stage issuers (auth.berget.ai, auth.stage.berget.ai) so no stale hostname references linger. No behavior change. --- src/auth/__tests__/issuer.test.ts | 20 ++++++++++---------- src/auth/__tests__/pkce-flow.test.ts | 6 +++--- src/auth/__tests__/token-refresh.test.ts | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/auth/__tests__/issuer.test.ts b/src/auth/__tests__/issuer.test.ts index 3791d67..a5e2656 100644 --- a/src/auth/__tests__/issuer.test.ts +++ b/src/auth/__tests__/issuer.test.ts @@ -23,17 +23,17 @@ describe('getConfiguration', () => { const authConfig = { apiBaseUrl: 'https://api.berget.ai', clientId: 'berget-code', - keycloakUrl: 'https://keycloak.berget.ai', + keycloakUrl: 'https://auth.berget.ai', realm: 'berget', }; const result1 = await getConfiguration(authConfig); - expect((result1 as any)._url).toBe('https://keycloak.berget.ai/realms/berget'); + expect((result1 as any)._url).toBe('https://auth.berget.ai/realms/berget'); expect(mockDiscoveryCalls).toHaveLength(1); // Second call should use cache const result2 = await getConfiguration(authConfig); - expect((result2 as any)._url).toBe('https://keycloak.berget.ai/realms/berget'); + expect((result2 as any)._url).toBe('https://auth.berget.ai/realms/berget'); expect(mockDiscoveryCalls).toHaveLength(1); // no additional call }); @@ -41,7 +41,7 @@ describe('getConfiguration', () => { const authConfig = { apiBaseUrl: 'https://api.berget.ai', clientId: 'berget-code', - keycloakUrl: 'https://keycloak.berget.ai', + keycloakUrl: 'https://auth.berget.ai', realm: 'berget', }; @@ -52,11 +52,11 @@ describe('getConfiguration', () => { const stageConfig = { ...authConfig, - keycloakUrl: 'https://keycloak.stage.berget.ai', + keycloakUrl: 'https://auth.stage.berget.ai', }; const result = await getConfiguration(stageConfig); - expect((result as any)._url).toBe('https://keycloak.stage.berget.ai/realms/berget'); + expect((result as any)._url).toBe('https://auth.stage.berget.ai/realms/berget'); expect(mockDiscoveryCalls).toHaveLength(2); }); @@ -64,22 +64,22 @@ describe('getConfiguration', () => { const prodConfig = { apiBaseUrl: 'https://api.berget.ai', clientId: 'berget-code', - keycloakUrl: 'https://keycloak.berget.ai', + keycloakUrl: 'https://auth.berget.ai', realm: 'berget', }; const stageConfig = { ...prodConfig, - keycloakUrl: 'https://keycloak.stage.berget.ai', + keycloakUrl: 'https://auth.stage.berget.ai', }; // First call discovers stage const stageResult = await getConfiguration(stageConfig); - expect((stageResult as any)._url).toBe('https://keycloak.stage.berget.ai/realms/berget'); + expect((stageResult as any)._url).toBe('https://auth.stage.berget.ai/realms/berget'); // Second call with prod must NOT return stage config from cache const prodResult = await getConfiguration(prodConfig); - expect((prodResult as any)._url).toBe('https://keycloak.berget.ai/realms/berget'); + expect((prodResult as any)._url).toBe('https://auth.berget.ai/realms/berget'); expect(mockDiscoveryCalls).toHaveLength(2); // two distinct discoveries }); }); diff --git a/src/auth/__tests__/pkce-flow.test.ts b/src/auth/__tests__/pkce-flow.test.ts index 30dcc8d..ad3cdb2 100644 --- a/src/auth/__tests__/pkce-flow.test.ts +++ b/src/auth/__tests__/pkce-flow.test.ts @@ -85,7 +85,7 @@ vi.mock('openid-client', async () => { return { authorizationCodeGrant: vi.fn(), buildAuthorizationUrl: vi.fn((_config, params) => { - const url = new URL('https://keycloak.berget.ai/realms/berget/protocol/openid-connect/auth'); + const url = new URL('https://auth.berget.ai/realms/berget/protocol/openid-connect/auth'); for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value as string); } @@ -304,7 +304,7 @@ describe('startPkceFlow', () => { // Keycloak sends extra params like iss and session_state const req = { - url: '/callback?code=authcode123&state=mock-state-uuid&iss=https%3A%2F%2Fkeycloak.berget.ai%2Frealms%2Fberget&session_state=abc-def', + url: '/callback?code=authcode123&state=mock-state-uuid&iss=https%3A%2F%2Fauth.berget.ai%2Frealms%2Fberget&session_state=abc-def', }; const res = { end: vi.fn(), writeHead: vi.fn() }; mockServer._triggerRequest(req, res); @@ -315,7 +315,7 @@ describe('startPkceFlow', () => { const passedUrl = (authorizationCodeGrant as ReturnType).mock.calls[0][1] as URL; expect(passedUrl.searchParams.get('code')).toBe('authcode123'); expect(passedUrl.searchParams.get('state')).toBe('mock-state-uuid'); - expect(passedUrl.searchParams.get('iss')).toBe('https://keycloak.berget.ai/realms/berget'); + expect(passedUrl.searchParams.get('iss')).toBe('https://auth.berget.ai/realms/berget'); expect(passedUrl.searchParams.get('session_state')).toBe('abc-def'); }); }); diff --git a/src/auth/__tests__/token-refresh.test.ts b/src/auth/__tests__/token-refresh.test.ts index f7d556c..1228e44 100644 --- a/src/auth/__tests__/token-refresh.test.ts +++ b/src/auth/__tests__/token-refresh.test.ts @@ -127,8 +127,8 @@ describe('refreshAccessToken', () => { const storeB = createMockStore({ _data: { access_token: 'old-b', expires_at: 1, refresh_token: 'refresh-b' }, }); - const configA = { issuer: 'https://keycloak.berget.ai' } as any; - const configB = { issuer: 'https://keycloak.stage.berget.ai' } as any; + const configA = { issuer: 'https://auth.berget.ai' } as any; + const configB = { issuer: 'https://auth.stage.berget.ai' } as any; mockRefreshTokenGrantResult = { access_token: 'new-token',