From 407324809213e04bbd7084f9baa2168a0e17fb77 Mon Sep 17 00:00:00 2001 From: Wenqi He Date: Tue, 18 Nov 2025 13:17:30 -0600 Subject: [PATCH 1/3] enable integration with external keycloak clients --- backend/app/routers/keycloak.py | 6 +++-- backend/app/routers/utils.py | 24 ++++++++++++++++--- .../src/openapi/v2/services/AuthService.ts | 6 +++++ openapi.json | 18 ++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/backend/app/routers/keycloak.py b/backend/app/routers/keycloak.py index bc244618a..d57bcae99 100644 --- a/backend/app/routers/keycloak.py +++ b/backend/app/routers/keycloak.py @@ -123,8 +123,10 @@ async def auth(code: str) -> RedirectResponse: @router.get("/token") -async def token(code: str): - return await get_token(code) +async def token(code: str, client_id: str, auth_redirect_uri: str): + return await get_token( + code, client_id=client_id, auth_redirect_uri=auth_redirect_uri + ) @router.get("/refresh_token") diff --git a/backend/app/routers/utils.py b/backend/app/routers/utils.py index 5e345a6e8..eb6d4df65 100644 --- a/backend/app/routers/utils.py +++ b/backend/app/routers/utils.py @@ -1,8 +1,9 @@ import mimetypes from typing import Optional +from keycloak import KeycloakOpenID + from app.config import settings -from app.keycloak_auth import keycloak_openid from app.models.files import ContentType from app.models.tokens import TokenDB from app.models.users import UserDB @@ -40,12 +41,29 @@ async def save_refresh_token(refresh_token: str, email: str): await token_created.insert() -async def get_token(code: str): +async def get_token( + code: str, + *, + server_url=settings.auth_server_url, + client_id=settings.auth_client_id, + realm_name=settings.auth_realm, + client_secret_key=settings.auth_client_secret, + auth_redirect_uri=settings.auth_redirect_uri, + verify=True, +): + keycloak_openid = KeycloakOpenID( + server_url=server_url, + client_id=client_id, + realm_name=realm_name, + client_secret_key=client_secret_key, + verify=verify, + ) + # get token from Keycloak token_body = keycloak_openid.token( grant_type="authorization_code", code=code, - redirect_uri=settings.auth_redirect_uri, + redirect_uri=auth_redirect_uri, ) access_token = token_body["access_token"] diff --git a/frontend/src/openapi/v2/services/AuthService.ts b/frontend/src/openapi/v2/services/AuthService.ts index fbbf716cd..d916c40fa 100644 --- a/frontend/src/openapi/v2/services/AuthService.ts +++ b/frontend/src/openapi/v2/services/AuthService.ts @@ -91,17 +91,23 @@ export class AuthService { /** * Token * @param code + * @param clientId + * @param authRedirectUri * @returns any Successful Response * @throws ApiError */ public static tokenApiV2AuthTokenGet( code: string, + clientId: string, + authRedirectUri: string, ): CancelablePromise { return __request({ method: 'GET', path: `/api/v2/auth/token`, query: { 'code': code, + 'client_id': clientId, + 'auth_redirect_uri': authRedirectUri, }, errors: { 422: `Validation Error`, diff --git a/openapi.json b/openapi.json index 849fee1f2..2d4aeb659 100644 --- a/openapi.json +++ b/openapi.json @@ -12074,6 +12074,24 @@ }, "name": "code", "in": "query" + }, + { + "required": true, + "schema": { + "title": "Client Id", + "type": "string" + }, + "name": "client_id", + "in": "query" + }, + { + "required": true, + "schema": { + "title": "Auth Redirect Uri", + "type": "string" + }, + "name": "auth_redirect_uri", + "in": "query" } ], "responses": { From f8be082d5e68d7fc495dbfe3581bb45935072431 Mon Sep 17 00:00:00 2001 From: Wenqi He Date: Wed, 10 Jun 2026 18:41:42 -0500 Subject: [PATCH 2/3] feat: add /token/external endpoint for third-party Keycloak integration Restore original /token endpoint unchanged. Add /token/external that accepts optional client_id, redirect_uri, server_url, realm_name, and client_secret_key, falling back to Clowder's own settings for any omitted params. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routers/keycloak.py | 22 +++++- backend/app/routers/utils.py | 20 ++--- .../src/openapi/v2/services/AuthService.ts | 40 ++++++++-- openapi.json | 73 ++++++++++++++++++- 4 files changed, 135 insertions(+), 20 deletions(-) diff --git a/backend/app/routers/keycloak.py b/backend/app/routers/keycloak.py index d57bcae99..aaa896d5b 100644 --- a/backend/app/routers/keycloak.py +++ b/backend/app/routers/keycloak.py @@ -1,6 +1,7 @@ import json import logging from secrets import token_urlsafe +from typing import Optional import requests from app.config import settings @@ -123,9 +124,26 @@ async def auth(code: str) -> RedirectResponse: @router.get("/token") -async def token(code: str, client_id: str, auth_redirect_uri: str): +async def token(code: str): + return await get_token(code) + + +@router.get("/token/external") +async def token_external( + code: str, + server_url: Optional[str] = None, + client_id: Optional[str] = None, + realm_name: Optional[str] = None, + client_secret_key: Optional[str] = None, + redirect_uri: Optional[str] = None, +): return await get_token( - code, client_id=client_id, auth_redirect_uri=auth_redirect_uri + code, + server_url=server_url, + client_id=client_id, + realm_name=realm_name, + client_secret_key=client_secret_key, + redirect_uri=redirect_uri, ) diff --git a/backend/app/routers/utils.py b/backend/app/routers/utils.py index eb6d4df65..bae990f15 100644 --- a/backend/app/routers/utils.py +++ b/backend/app/routers/utils.py @@ -44,18 +44,18 @@ async def save_refresh_token(refresh_token: str, email: str): async def get_token( code: str, *, - server_url=settings.auth_server_url, - client_id=settings.auth_client_id, - realm_name=settings.auth_realm, - client_secret_key=settings.auth_client_secret, - auth_redirect_uri=settings.auth_redirect_uri, + server_url=None, + client_id=None, + realm_name=None, + client_secret_key=None, + redirect_uri=None, verify=True, ): keycloak_openid = KeycloakOpenID( - server_url=server_url, - client_id=client_id, - realm_name=realm_name, - client_secret_key=client_secret_key, + server_url=server_url or settings.auth_server_url, + client_id=client_id or settings.auth_client_id, + realm_name=realm_name or settings.auth_realm, + client_secret_key=client_secret_key or settings.auth_client_secret, verify=verify, ) @@ -63,7 +63,7 @@ async def get_token( token_body = keycloak_openid.token( grant_type="authorization_code", code=code, - redirect_uri=auth_redirect_uri, + redirect_uri=redirect_uri or settings.auth_redirect_uri, ) access_token = token_body["access_token"] diff --git a/frontend/src/openapi/v2/services/AuthService.ts b/frontend/src/openapi/v2/services/AuthService.ts index d916c40fa..94c34f103 100644 --- a/frontend/src/openapi/v2/services/AuthService.ts +++ b/frontend/src/openapi/v2/services/AuthService.ts @@ -91,23 +91,53 @@ export class AuthService { /** * Token * @param code - * @param clientId - * @param authRedirectUri * @returns any Successful Response * @throws ApiError */ public static tokenApiV2AuthTokenGet( code: string, - clientId: string, - authRedirectUri: string, ): CancelablePromise { return __request({ method: 'GET', path: `/api/v2/auth/token`, query: { 'code': code, + }, + errors: { + 422: `Validation Error`, + }, + }); + } + + /** + * Token External + * @param code + * @param serverUrl + * @param clientId + * @param realmName + * @param clientSecretKey + * @param redirectUri + * @returns any Successful Response + * @throws ApiError + */ + public static tokenExternalApiV2AuthTokenExternalGet( + code: string, + serverUrl?: string, + clientId?: string, + realmName?: string, + clientSecretKey?: string, + redirectUri?: string, + ): CancelablePromise { + return __request({ + method: 'GET', + path: `/api/v2/auth/token/external`, + query: { + 'code': code, + 'server_url': serverUrl, 'client_id': clientId, - 'auth_redirect_uri': authRedirectUri, + 'realm_name': realmName, + 'client_secret_key': clientSecretKey, + 'redirect_uri': redirectUri, }, errors: { 422: `Validation Error`, diff --git a/openapi.json b/openapi.json index 2d4aeb659..ca6c05e98 100644 --- a/openapi.json +++ b/openapi.json @@ -12074,9 +12074,58 @@ }, "name": "code", "in": "query" + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v2/auth/token/external": { + "get": { + "tags": [ + "auth" + ], + "summary": "Token External", + "operationId": "token_external_api_v2_auth_token_external_get", + "parameters": [ { "required": true, + "schema": { + "title": "Code", + "type": "string" + }, + "name": "code", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Server Url", + "type": "string" + }, + "name": "server_url", + "in": "query" + }, + { + "required": false, "schema": { "title": "Client Id", "type": "string" @@ -12085,12 +12134,30 @@ "in": "query" }, { - "required": true, + "required": false, + "schema": { + "title": "Realm Name", + "type": "string" + }, + "name": "realm_name", + "in": "query" + }, + { + "required": false, + "schema": { + "title": "Client Secret Key", + "type": "string" + }, + "name": "client_secret_key", + "in": "query" + }, + { + "required": false, "schema": { - "title": "Auth Redirect Uri", + "title": "Redirect Uri", "type": "string" }, - "name": "auth_redirect_uri", + "name": "redirect_uri", "in": "query" } ], From ac34907e7ae7e47a5f84b0dbbe1ad836c76ba685 Mon Sep 17 00:00:00 2001 From: Wenqi He Date: Thu, 11 Jun 2026 06:33:03 -0500 Subject: [PATCH 3/3] refactor: simplify token endpoint to accept optional redirect_uri MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop /token/external — the same Keycloak client supports multiple redirect URIs, so only redirect_uri needs to vary per caller. get_token now takes only redirect_uri as an override, using the module-level keycloak_openid instance directly. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routers/keycloak.py | 21 +---- backend/app/routers/utils.py | 24 +----- .../src/openapi/v2/services/AuthService.ts | 37 +-------- openapi.json | 76 ------------------- 4 files changed, 7 insertions(+), 151 deletions(-) diff --git a/backend/app/routers/keycloak.py b/backend/app/routers/keycloak.py index aaa896d5b..1f0ac5128 100644 --- a/backend/app/routers/keycloak.py +++ b/backend/app/routers/keycloak.py @@ -124,26 +124,9 @@ async def auth(code: str) -> RedirectResponse: @router.get("/token") -async def token(code: str): - return await get_token(code) - - -@router.get("/token/external") -async def token_external( - code: str, - server_url: Optional[str] = None, - client_id: Optional[str] = None, - realm_name: Optional[str] = None, - client_secret_key: Optional[str] = None, - redirect_uri: Optional[str] = None, -): +async def token(code: str, redirect_uri: Optional[str] = None): return await get_token( - code, - server_url=server_url, - client_id=client_id, - realm_name=realm_name, - client_secret_key=client_secret_key, - redirect_uri=redirect_uri, + code, redirect_uri=redirect_uri or settings.auth_redirect_uri ) diff --git a/backend/app/routers/utils.py b/backend/app/routers/utils.py index bae990f15..b5a3c6a1c 100644 --- a/backend/app/routers/utils.py +++ b/backend/app/routers/utils.py @@ -1,9 +1,8 @@ import mimetypes from typing import Optional -from keycloak import KeycloakOpenID - from app.config import settings +from app.keycloak_auth import keycloak_openid from app.models.files import ContentType from app.models.tokens import TokenDB from app.models.users import UserDB @@ -41,29 +40,12 @@ async def save_refresh_token(refresh_token: str, email: str): await token_created.insert() -async def get_token( - code: str, - *, - server_url=None, - client_id=None, - realm_name=None, - client_secret_key=None, - redirect_uri=None, - verify=True, -): - keycloak_openid = KeycloakOpenID( - server_url=server_url or settings.auth_server_url, - client_id=client_id or settings.auth_client_id, - realm_name=realm_name or settings.auth_realm, - client_secret_key=client_secret_key or settings.auth_client_secret, - verify=verify, - ) - +async def get_token(code: str, *, redirect_uri: str = settings.auth_redirect_uri): # get token from Keycloak token_body = keycloak_openid.token( grant_type="authorization_code", code=code, - redirect_uri=redirect_uri or settings.auth_redirect_uri, + redirect_uri=redirect_uri, ) access_token = token_body["access_token"] diff --git a/frontend/src/openapi/v2/services/AuthService.ts b/frontend/src/openapi/v2/services/AuthService.ts index 94c34f103..ecac0e7ff 100644 --- a/frontend/src/openapi/v2/services/AuthService.ts +++ b/frontend/src/openapi/v2/services/AuthService.ts @@ -91,52 +91,19 @@ export class AuthService { /** * Token * @param code - * @returns any Successful Response - * @throws ApiError - */ - public static tokenApiV2AuthTokenGet( - code: string, - ): CancelablePromise { - return __request({ - method: 'GET', - path: `/api/v2/auth/token`, - query: { - 'code': code, - }, - errors: { - 422: `Validation Error`, - }, - }); - } - - /** - * Token External - * @param code - * @param serverUrl - * @param clientId - * @param realmName - * @param clientSecretKey * @param redirectUri * @returns any Successful Response * @throws ApiError */ - public static tokenExternalApiV2AuthTokenExternalGet( + public static tokenApiV2AuthTokenGet( code: string, - serverUrl?: string, - clientId?: string, - realmName?: string, - clientSecretKey?: string, redirectUri?: string, ): CancelablePromise { return __request({ method: 'GET', - path: `/api/v2/auth/token/external`, + path: `/api/v2/auth/token`, query: { 'code': code, - 'server_url': serverUrl, - 'client_id': clientId, - 'realm_name': realmName, - 'client_secret_key': clientSecretKey, 'redirect_uri': redirectUri, }, errors: { diff --git a/openapi.json b/openapi.json index ca6c05e98..c05e10500 100644 --- a/openapi.json +++ b/openapi.json @@ -12074,82 +12074,6 @@ }, "name": "code", "in": "query" - } - ], - "responses": { - "200": { - "description": "Successful Response", - "content": { - "application/json": { - "schema": {} - } - } - }, - "422": { - "description": "Validation Error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/HTTPValidationError" - } - } - } - } - } - } - }, - "/api/v2/auth/token/external": { - "get": { - "tags": [ - "auth" - ], - "summary": "Token External", - "operationId": "token_external_api_v2_auth_token_external_get", - "parameters": [ - { - "required": true, - "schema": { - "title": "Code", - "type": "string" - }, - "name": "code", - "in": "query" - }, - { - "required": false, - "schema": { - "title": "Server Url", - "type": "string" - }, - "name": "server_url", - "in": "query" - }, - { - "required": false, - "schema": { - "title": "Client Id", - "type": "string" - }, - "name": "client_id", - "in": "query" - }, - { - "required": false, - "schema": { - "title": "Realm Name", - "type": "string" - }, - "name": "realm_name", - "in": "query" - }, - { - "required": false, - "schema": { - "title": "Client Secret Key", - "type": "string" - }, - "name": "client_secret_key", - "in": "query" }, { "required": false,