fix: configure Kong JWT auth flow with consumer credentials

- Add kid claim to auth-service JWT for Kong validation
- Add Kong consumer with JWT credential (shared secret via env)
- Add agent-config route to Kong for /api/v1/agent-config
- Kong Dockerfile uses entrypoint script to inject JWT_SECRET at runtime
- Fix frontend login path (/auth/login → /api/v1/auth/login)
- Extract tenantId from JWT on login and store as current_tenant
- Add auth guard in admin layout (redirect to /login if no token)
- Pass JWT_SECRET env var to Kong container in docker-compose

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-02-21 23:20:06 -08:00
parent 28131491e2
commit 48e47975ca
7 changed files with 55 additions and 3 deletions

View File

@ -40,6 +40,8 @@ services:
build: build:
context: ../../packages/gateway context: ../../packages/gateway
container_name: it0-api-gateway container_name: it0-api-gateway
environment:
- JWT_SECRET=${JWT_SECRET:-dev-jwt-secret}
ports: ports:
- "18000:8000" - "18000:8000"
- "18001:8001" - "18001:8001"

View File

@ -1,9 +1,25 @@
'use client'; 'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Sidebar } from '@/presentation/components/layout/sidebar'; import { Sidebar } from '@/presentation/components/layout/sidebar';
import { TopBar } from '@/presentation/components/layout/top-bar'; import { TopBar } from '@/presentation/components/layout/top-bar';
export default function AdminLayout({ children }: { children: React.ReactNode }) { export default function AdminLayout({ children }: { children: React.ReactNode }) {
const router = useRouter();
const [ready, setReady] = useState(false);
useEffect(() => {
const token = localStorage.getItem('access_token');
if (!token) {
router.replace('/login');
} else {
setReady(true);
}
}, [router]);
if (!ready) return null;
return ( return (
<div className="flex h-screen"> <div className="flex h-screen">
<Sidebar /> <Sidebar />

View File

@ -23,7 +23,7 @@ export default function LoginPage() {
setError(null); setError(null);
try { try {
const data = await apiClient<LoginResponse>('/auth/login', { const data = await apiClient<LoginResponse>('/api/v1/auth/login', {
method: 'POST', method: 'POST',
body: { email, password }, body: { email, password },
}); });
@ -32,6 +32,14 @@ export default function LoginPage() {
localStorage.setItem('refresh_token', data.refreshToken); localStorage.setItem('refresh_token', data.refreshToken);
localStorage.setItem('user', JSON.stringify(data.user)); localStorage.setItem('user', JSON.stringify(data.user));
// Decode tenantId from JWT and store as current tenant
try {
const payload = JSON.parse(atob(data.accessToken.split('.')[1]));
if (payload.tenantId) {
localStorage.setItem('current_tenant', JSON.stringify({ id: payload.tenantId }));
}
} catch { /* ignore decode errors */ }
router.push('/dashboard'); router.push('/dashboard');
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Login failed'); setError(err instanceof Error ? err.message : 'Login failed');

View File

@ -1,9 +1,13 @@
FROM kong:3.7 FROM kong:3.7
COPY config/kong.yml /etc/kong/kong.yml COPY config/kong.yml /etc/kong/kong.yml.tpl
COPY config/docker-entrypoint.sh /custom-entrypoint.sh
RUN chmod +x /custom-entrypoint.sh
ENV KONG_DATABASE=off ENV KONG_DATABASE=off
ENV KONG_DECLARATIVE_CONFIG=/etc/kong/kong.yml ENV KONG_DECLARATIVE_CONFIG=/etc/kong/kong.yml
ENV KONG_PROXY_LISTEN=0.0.0.0:8000 ENV KONG_PROXY_LISTEN=0.0.0.0:8000
ENV KONG_ADMIN_LISTEN=0.0.0.0:8001 ENV KONG_ADMIN_LISTEN=0.0.0.0:8001
ENV KONG_LOG_LEVEL=info ENV KONG_LOG_LEVEL=info
ENTRYPOINT ["/custom-entrypoint.sh"]

View File

@ -0,0 +1,4 @@
#!/bin/sh
# Substitute JWT_SECRET into Kong declarative config at runtime
sed "s/\${JWT_SECRET}/${JWT_SECRET:-dev-jwt-secret}/g" /etc/kong/kong.yml.tpl > /etc/kong/kong.yml
exec /docker-entrypoint.sh kong docker-start

View File

@ -1,5 +1,12 @@
_format_version: "3.0" _format_version: "3.0"
consumers:
- username: it0-system
jwt_secrets:
- key: it0-auth
algorithm: HS256
secret: "${JWT_SECRET}"
services: services:
- name: auth-service - name: auth-service
url: http://auth-service:3001 url: http://auth-service:3001
@ -24,6 +31,14 @@ services:
- http - http
- https - https
- name: agent-config-service
url: http://agent-service:3002
routes:
- name: agent-config-routes
paths:
- /api/v1/agent-config
strip_path: false
- name: ops-service - name: ops-service
url: http://ops-service:3003 url: http://ops-service:3003
routes: routes:

View File

@ -153,7 +153,10 @@ export class AuthService {
roles: user.roles, roles: user.roles,
}; };
const accessToken = this.jwtService.sign(jwtPayload); const accessToken = this.jwtService.sign({
...jwtPayload,
kid: 'it0-auth',
});
const refreshToken = this.jwtService.sign( const refreshToken = this.jwtService.sign(
{ sub: user.id, type: 'refresh' }, { sub: user.id, type: 'refresh' },