import { test, expect } from '@playwright/test'; /** * Auth integration tests. * * Validates that: * 1. auth.researchstack.info serves the Authentik UI * 2. SSO-protected paths are intercepted by Authentik forward_auth * 3. /api/* paths do NOT go through forward_auth (token-auth only) * 4. Authentik outpost forward_auth is wired correctly * * State awareness: * - Authentik server is running (auth.researchstack.info responds) * - Authentik embedded outpost may not have a configured application yet * - When outpost has no matching app, forward_auth returns 404 with X-Authentik-Id * - This is distinct from Traefik 404 (no matching Ingress rule) */ /** Returns true if the response came from Authentik (vs Traefik or backend) */ function isAuthentikResponse(headers: Record): boolean { return 'x-authentik-id' in headers || headers['x-powered-by'] === 'authentik'; } test.describe('Authentik SSO at auth.researchstack.info', () => { test('auth subdomain serves Authentik login page', async ({ request }) => { const response = await request.get('https://auth.researchstack.info/', { maxRedirects: 5, }); expect(response.status()).toBeLessThan(500); // Authentik login page should contain typical markers const content = await response.text(); const isAuthentik = content.includes('authentik') || content.includes('Authentik') || content.includes('ak-flow') || content.includes('Sign in') || content.includes('flow/default'); expect(isAuthentik).toBe(true); }); test('auth subdomain OIDC discovery endpoint exists', async ({ request }) => { const response = await request.get( 'https://auth.researchstack.info/application/o/.well-known/openid-configuration' ); // Might be 404 if no application configured, or 200 with OIDC metadata if (response.status() === 200) { const body = await response.json(); expect(body).toHaveProperty('issuer'); expect(body.issuer).toContain('auth.researchstack.info'); } else { // At minimum, should not be a redirect or 500 expect(response.status()).toBeLessThan(500); } }); }); test.describe('forward_auth intercepts SSO-protected paths', () => { const protectedPaths = [ '/apps/chat/', '/apps/budget/', '/server/status/', '/server/dash/', '/server/vault/', '/', ]; for (const path of protectedPaths) { test(`${path} is intercepted by Authentik forward_auth`, async ({ request }) => { const response = await request.get(path, { maxRedirects: 0, }); const headers = response.headers(); // The key assertion: Authentik forward_auth middleware IS in the path. // Evidence of this: // - 302/303 redirect to auth.researchstack.info (outpost configured, redirecting to login) // - 404 with X-Authentik-Id header (outpost reached, but no matching app configured yet) // - 401/403 (auth denial from outpost) // - 200 (user is already authenticated or auth not enforced) if (response.status() === 302 || response.status() === 303) { const location = headers['location']; expect(location).toContain('auth.researchstack.info'); } else if (response.status() === 404) { // If 404, it should be from Authentik (outpost not configured), not from Traefik expect(isAuthentikResponse(headers)).toBe(true); } else { // 200 (no auth enforced) or 401/403 (denied) expect([200, 401, 403]).toContain(response.status()); } }); } }); test.describe('/api/* paths bypass forward_auth', () => { const apiPaths = [ '/api/cred/', '/api/registry/health', '/api/jobs/health', '/api/blobs/health', ]; for (const path of apiPaths) { test(`${path} does NOT redirect to Authentik`, async ({ request }) => { const response = await request.get(path, { maxRedirects: 0, }); // API paths should NEVER redirect to Authentik if (response.status() === 302 || response.status() === 303) { const location = response.headers()['location'] || ''; expect(location).not.toContain('auth.researchstack.info'); } // Valid responses: 200 (service up), 401/403 (token auth), 502/503 (not deployed) expect([200, 401, 403, 404, 502, 503]).toContain(response.status()); }); } }); test.describe('X-Forwarded headers propagation', () => { test('edge correctly proxies to internal services over tailnet', async ({ request }) => { // The /api/registry/health endpoint (if deployed) should respond // This validates the full edge → traefik → service path works const response = await request.get('/api/registry/health'); if (response.status() === 200) { const body = await response.json(); expect(body.status).toBe('ok'); } // If not deployed, just verify we don't get a TLS error expect(response.status()).toBeLessThan(600); }); });