From c33e1436d3dbbec8908373677e8c0124ede2854d Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 27 Oct 2026 12:20:57 +0000 Subject: [PATCH 1/2] changed file name for local realm, disabled its import --- docker-compose.yml | 4 ++-- dev-realm.json => knowledge-hub-dev-realm.json | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename dev-realm.json => knowledge-hub-dev-realm.json (100%) diff --git a/docker-compose.yml b/docker-compose.yml index 5f19c38..c23e610 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: keycloak: image: quay.io/keycloak/keycloak:latest - command: start-dev --import-realm + command: start-dev environment: KEYCLOAK_ADMIN: admin KEYCLOAK_ADMIN_PASSWORD: admin @@ -14,7 +14,7 @@ services: - "8080:8080" volumes: - keycloak_data:/opt/keycloak/data - - ./dev-realm.json:/opt/keycloak/data/import/dev-realm.json + - ./knowledge-hub-dev-realm.json:/opt/keycloak/data/import/knowledge-hub-dev-realm.json db: image: postgres:16 diff --git a/dev-realm.json b/knowledge-hub-dev-realm.json similarity index 100% rename from dev-realm.json rename to knowledge-hub-dev-realm.json From 1d18194c104c68a3119f7bb0bf4ff03dafdd576f Mon Sep 17 00:00:00 2001 From: lkrone Date: Tue, 27 Oct 2026 12:21:21 +0000 Subject: [PATCH 2/2] use refresh token --- backend/app/routers/auth.py | 37 +++++++++++++++++++++++++-- frontend/src/api/api.ts | 41 ++++++++++++++++++++++++++++++ frontend/src/auth/AuthProvider.tsx | 17 +++++++++++++ 3 files changed, 93 insertions(+), 2 deletions(-) diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 65d77b0..e2c219a 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,7 +1,8 @@ -from fastapi import Request, APIRouter +from fastapi import Request, APIRouter, HTTPException from fastapi.responses import RedirectResponse from ..crud.keycloak import oauth from ..config import get_settings +import httpx2 from urllib.parse import urlencode @@ -36,4 +37,36 @@ async def logout(id_token: str | None = None): else: params["client_id"] = settings.keycloak_client_id - return RedirectResponse(f"{end_session_endpoint}?{urlencode(params)}") \ No newline at end of file + return RedirectResponse(f"{end_session_endpoint}?{urlencode(params)}") + + +@auth_router.post("/auth/refresh") +async def refresh(request: Request): + """Refresh access token using Keycloak refresh token. + + Expects JSON body: {"refresh_token": "..."} + Returns new tokens (access_token, refresh_token, id_token). + """ + body = await request.json() + refresh_token = body.get("refresh_token") + if not refresh_token: + raise HTTPException(status_code=400, detail="refresh_token missing") + token_url = f"{settings.keycloak_internal_url}/realms/{settings.keycloak_realm}/protocol/openid-connect/token" + async with httpx2.AsyncClient() as client: + resp = await client.post( + token_url, + data={ + "grant_type": "refresh_token", + "client_id": settings.keycloak_client_id, + "client_secret": settings.keycloak_client_secret, + "refresh_token": refresh_token, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + ) + resp.raise_for_status() + token = resp.json() + return { + "access_token": token.get("access_token"), + "refresh_token": token.get("refresh_token", ""), + "id_token": token.get("id_token", ""), + } diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index 1f7038b..6b94c2f 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -51,6 +51,39 @@ async function request(path: string, opts: RequestInit = {}): Promise { const res = await fetch(`${BASE}${path}`, {...opts, headers}); if (!res.ok) { + // Attempt automatic token refresh on 401 + if (res.status === 401) { + const refresh = localStorage.getItem('refresh_token'); + if (refresh) { + try { + const newTokens = await api.refreshToken(refresh); + // Update stored tokens + localStorage.setItem('access_token', newTokens.access_token); + if (newTokens.refresh_token) localStorage.setItem('refresh_token', newTokens.refresh_token); + if (newTokens.id_token) localStorage.setItem('id_token', newTokens.id_token); + // Retry the original request with new access token + const retryHeaders = new Headers(opts.headers); + retryHeaders.set('Authorization', `Bearer ${newTokens.access_token}`); + const retryRes = await fetch(`${BASE}${path}`, {...opts, headers: retryHeaders}); + if (retryRes.ok) { + if (retryRes.status === 204) return null as T; + return retryRes.json() as Promise; + } + // If retry also fails, fall through to error handling below + let retryDetail: unknown; + try { + retryDetail = await retryRes.json(); + } catch { + retryDetail = await retryRes.text(); + } + throw new ApiError(retryRes.status, retryDetail); + } catch (refreshErr) { + // Refresh failed – proceed to original error handling + console.error('Token refresh failed:', refreshErr); + } + } + } + // Original error handling let detail: unknown; try { detail = await res.json(); @@ -66,6 +99,7 @@ async function request(path: string, opts: RequestInit = {}): Promise { export const api = { // ---Auth--- login: (username: string, password: string) => { + // existing login remains const formData = new FormData(); formData.append('username', username); formData.append('password', password); @@ -75,6 +109,13 @@ export const api = { body: formData, }); }, + // Refresh token using stored refresh token + refreshToken: (refreshToken: string) => + request<{ access_token: string; refresh_token: string; id_token: string }>("/auth/refresh", { + method: "POST", + body: JSON.stringify({ refresh_token: refreshToken }), + headers: { "Content-Type": "application/json" }, + }), register: (data: UserCreate) => request("/register", { diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx index 3179a12..3949a2c 100644 --- a/frontend/src/auth/AuthProvider.tsx +++ b/frontend/src/auth/AuthProvider.tsx @@ -38,6 +38,23 @@ export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => setUser(userData); } catch (error) { console.error('Fehler beim Laden des Benutzers:', error); + // Try to refresh token if possible + const refresh = localStorage.getItem('refresh_token'); + if (refresh) { + try { + const newTokens = await api.refreshToken(refresh); + localStorage.setItem('access_token', newTokens.access_token); + if (newTokens.refresh_token) localStorage.setItem('refresh_token', newTokens.refresh_token); + if (newTokens.id_token) localStorage.setItem('id_token', newTokens.id_token); + // Retry fetching user + const userData = await api.getCurrentUser(); + setUser(userData); + setIsLoading(false); + return; + } catch (refreshError) { + console.error('Refresh failed:', refreshError); + } + } logout(); } finally { setIsLoading(false);