From b76fce3df55180235a494d6dca00cf15b43973eb Mon Sep 17 00:00:00 2001 From: jocelyneholdbrook Date: Wed, 21 Jan 2026 15:38:31 +0000 Subject: [PATCH] feat(medcat-trainer)!: Consolidate OIDC env vars BREAKING CHANGE: Previous env vars have been renamed and will no longer work. --- medcat-trainer/OIDC_AUTHENTICATION_GUIDE.md | 12 ++--- medcat-trainer/docker-compose-dev.yml | 20 +++----- medcat-trainer/docs/installation.md | 40 +++++++-------- medcat-trainer/webapp/api/core/settings.py | 50 ++++++++++++++----- .../frontend/public/config.template.json | 12 +++-- medcat-trainer/webapp/frontend/src/App.vue | 2 +- medcat-trainer/webapp/frontend/src/auth.ts | 8 +-- .../webapp/frontend/src/runtimeConfig.ts | 4 +- .../webapp/scripts/load_examples.py | 2 +- .../webapp/scripts/nginx-entrypoint.sh | 42 +++++++++------- 10 files changed, 108 insertions(+), 84 deletions(-) diff --git a/medcat-trainer/OIDC_AUTHENTICATION_GUIDE.md b/medcat-trainer/OIDC_AUTHENTICATION_GUIDE.md index 218a82ce1..a2feb85a7 100644 --- a/medcat-trainer/OIDC_AUTHENTICATION_GUIDE.md +++ b/medcat-trainer/OIDC_AUTHENTICATION_GUIDE.md @@ -39,7 +39,7 @@ MedCAT Trainer uses a **two-client architecture** for OIDC: "KEYCLOAK_URL": "https://auth.cogstack.example.site", "KEYCLOAK_REALM": "cogstack", "KEYCLOAK_CLIENT_ID": "cogstack-medcattrainer-frontend", - "LOGOUT_REDIRECT_URI": "https://launchpad.cogstack.example.site/" + "KEYCLOAK_LOGOUT_REDIRECT_URI": "https://launchpad.cogstack.example.site/" } ``` @@ -57,11 +57,11 @@ MedCAT Trainer uses a **two-client architecture** for OIDC: **Configuration:** ```python # Backend settings -OIDC_HOST = "https://auth.cogstack.example.site" -OIDC_REALM = "cogstack" -OIDC_FRONTEND_CLIENT_ID = "cogstack-medcattrainer-frontend" -OIDC_BACKEND_CLIENT_ID = "cogstack-medcattrainer-backend" -OIDC_BACKEND_CLIENT_SECRET = "***secret***" +KEYCLOAK_INTERNAL_SERVICE_URL = "https://auth.cogstack.example.site" +KEYCLOAK_REALM = "cogstack" +KEYCLOAK_FRONTEND_CLIENT_ID = "cogstack-medcattrainer-frontend" +KEYCLOAK_BACKEND_CLIENT_ID = "cogstack-medcattrainer-backend" +KEYCLOAK_BACKEND_CLIENT_SECRET = "***secret***" ``` --- ## Key Files diff --git a/medcat-trainer/docker-compose-dev.yml b/medcat-trainer/docker-compose-dev.yml index 26247c249..2e956bdf3 100644 --- a/medcat-trainer/docker-compose-dev.yml +++ b/medcat-trainer/docker-compose-dev.yml @@ -24,19 +24,15 @@ services: - ./envs/env environment: - MCT_VERSION=latest - # Frontend OIDC Settings (Public Client) - - VITE_USE_OIDC=1 - - VITE_KEYCLOAK_URL=http://keycloak.cogstack.localhost - - VITE_KEYCLOAK_REALM=cogstack-realm - - VITE_KEYCLOAK_CLIENT_ID=cogstack-medcattrainer-frontend - - VITE_LOGOUT_REDIRECT_URI=http://launch.cogstack.localhost/ - # Backend OIDC Settings (Confidential Client) + # OIDC Settings - USE_OIDC=1 - - OIDC_HOST=http://keycloak:8080 - - OIDC_REALM=cogstack-realm - - OIDC_FRONTEND_CLIENT_ID=cogstack-medcattrainer-frontend - - OIDC_BACKEND_CLIENT_ID=cogstack-medcattrainer-backend - - OIDC_BACKEND_CLIENT_SECRET=*** + - KEYCLOAK_URL=http://keycloak.cogstack.localhost + - KEYCLOAK_REALM=cogstack-realm + - KEYCLOAK_FRONTEND_CLIENT_ID=cogstack-medcattrainer-frontend + - KEYCLOAK_LOGOUT_REDIRECT_URI=http://launch.cogstack.localhost/ + - KEYCLOAK_INTERNAL_SERVICE_URL=http://keycloak:8080 + - KEYCLOAK_BACKEND_CLIENT_ID=cogstack-medcattrainer-backend + - KEYCLOAK_BACKEND_CLIENT_SECRET=*** command: /usr/bin/supervisord -c /etc/supervisord.conf nginx: diff --git a/medcat-trainer/docs/installation.md b/medcat-trainer/docs/installation.md index fc8fece90..c7818a7ee 100644 --- a/medcat-trainer/docs/installation.md +++ b/medcat-trainer/docs/installation.md @@ -77,31 +77,27 @@ You'll need to `docker stop` the running containers if you have already run the You can enable OIDC (OpenID Connect) authentication for the MedCAT Trainer. To do so, you must configure the following environment variables: -#### Frontend (Runtime Config) - -| Variable | Example | Description | -|----------|---------|-------------| -| `VITE_USE_OIDC` | `1` | Enable OIDC (1=enabled, 0=traditional auth) | -| `VITE_KEYCLOAK_URL` | `https://cogstack-auth.sites.er.kcl.ac.uk` | Keycloak base URL | -| `VITE_KEYCLOAK_REALM` | `cogstack` | Keycloak realm name | -| `VITE_KEYCLOAK_CLIENT_ID` | `cogstack-medcattrainer-frontend` | Public client ID | -| `VITE_LOGOUT_REDIRECT_URI` | `https://cogstack-launchpad.sites.er.kcl.ac.uk/` | Where to go after logout | - -#### Backend (Django Settings) - -| Variable | Example | Description | -|----------|---------|-------------| -| `USE_OIDC` | `1` | Enable OIDC validation | -| `OIDC_HOST` | `https://cogstack-auth.sites.er.kcl.ac.uk` | Keycloak base URL (for backend) | -| `OIDC_REALM` | `cogstack` | Realm name | -| `OIDC_FRONTEND_CLIENT_ID` | `cogstack-medcattrainer-frontend` | Frontend client ID (for token validation) | -| `OIDC_BACKEND_CLIENT_ID` | `cogstack-medcattrainer-backend` | Backend client ID | -| `OIDC_BACKEND_CLIENT_SECRET` | `***secret***` | Backend client secret | - +| Variable | Example | Description | +|-----------------------------------|-------------------------------------------|----------------------------------------------------| +| `USE_OIDC` | `1` | Enable OIDC (1=enabled, 0=traditional auth) | +| `KEYCLOAK_URL` | `https://auth.example.org` | Keycloak base URL | +| `KEYCLOAK_REALM` | `cogstack` | Keycloak realm name | +| `KEYCLOAK_LOGOUT_REDIRECT_URI` | `https://cogstack-launchpad.example.org/` | Where to go after logout | +| `KEYCLOAK_INTERNAL_SERVICE_URL` | `http://keycloak.8080` | Keycloak internal service URL | +| `KEYCLOAK_FRONTEND_CLIENT_ID` | `cogstack-medcattrainer-frontend` | Keycloak Frontend client ID (for token validation) | +| `KEYCLOAK_BACKEND_CLIENT_ID` | `cogstack-medcattrainer-backend` | Keycloak Backend client ID | +| `KEYCLOAK_BACKEND_CLIENT_SECRET` | `***secret***` | Keycloak Backend client secret | + +#### Advanced Optional OIDC Settings + +| Variable | Default | Description | +|-----------------------------------|---------|-----------------------------------------------------------------------------------| +| `KEYCLOAK_TOKEN_MIN_VALIDITY` | `30` | The interval in seconds between each refresh attempt | +| `KEYCLOAK_TOKEN_REFRESH_INTERVAL` | `20` | Minimum time in seconds the token should remain valid before triggering a refresh | You can either use the Gateway Auth stack available in cogstack-ops or deploy your own Keycloak instance. -If you deploy your own Keycloak instance, make sure to configure the network accordingly. +#### Roles Currently, there are two roles that can be assigned to users: | Keycloak Role | Django Permission | Capabilities | diff --git a/medcat-trainer/webapp/api/core/settings.py b/medcat-trainer/webapp/api/core/settings.py index dd1a2c381..e121b03a9 100644 --- a/medcat-trainer/webapp/api/core/settings.py +++ b/medcat-trainer/webapp/api/core/settings.py @@ -200,40 +200,64 @@ } if USE_OIDC: - log.info("Using OIDC authentication") + print("Using OIDC authentication") REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = [ 'oidc_auth.authentication.JSONWebTokenAuthentication', 'oidc_auth.authentication.BearerTokenAuthentication', ] - OIDC_HOST = os.environ.get('OIDC_HOST', '') - OIDC_REALM = os.environ.get('OIDC_REALM', default='cogstack-realm') - OIDC_BACKEND_CLIENT_ID = os.environ.get('OIDC_BACKEND_CLIENT_ID', default='cogstack-medcattrainer-backend') - OIDC_BACKEND_CLIENT_SECRET = os.environ.get('OIDC_BACKEND_CLIENT_SECRET', default='') - OIDC_FRONTEND_CLIENT_ID = os.environ.get('OIDC_FRONTEND_CLIENT_ID', default='cogstack-medcattrainer-frontend') + # Load OIDC configuration from environment + KEYCLOAK_INTERNAL_SERVICE_URL = os.environ.get('KEYCLOAK_INTERNAL_SERVICE_URL') + KEYCLOAK_REALM = os.environ.get('KEYCLOAK_REALM') + KEYCLOAK_BACKEND_CLIENT_ID = os.environ.get('KEYCLOAK_BACKEND_CLIENT_ID') + KEYCLOAK_BACKEND_CLIENT_SECRET = os.environ.get('KEYCLOAK_BACKEND_CLIENT_SECRET') + KEYCLOAK_FRONTEND_CLIENT_ID = os.environ.get('KEYCLOAK_FRONTEND_CLIENT_ID') + + # Validate required OIDC configuration + missing_vars = [] + if not KEYCLOAK_INTERNAL_SERVICE_URL: + missing_vars.append('KEYCLOAK_INTERNAL_SERVICE_URL') + if not KEYCLOAK_REALM: + missing_vars.append('KEYCLOAK_REALM') + if not KEYCLOAK_BACKEND_CLIENT_ID: + missing_vars.append('KEYCLOAK_BACKEND_CLIENT_ID') + if not KEYCLOAK_BACKEND_CLIENT_SECRET: + missing_vars.append('KEYCLOAK_BACKEND_CLIENT_SECRET') + if not KEYCLOAK_FRONTEND_CLIENT_ID: + missing_vars.append('KEYCLOAK_FRONTEND_CLIENT_ID') + + if missing_vars: + error_msg = ( + f"OIDC is enabled (USE_OIDC=1) but the following required " + f"environment variables are missing or empty: {', '.join(missing_vars)}\n" + f"Please set these variables in your environment configuration." + ) + log.error(error_msg) + sys.exit(error_msg) + OIDC_AUTH = { - 'OIDC_ENDPOINT': f"{OIDC_HOST}/realms/{OIDC_REALM}", + 'OIDC_ENDPOINT': f"{KEYCLOAK_INTERNAL_SERVICE_URL}/realms/{KEYCLOAK_REALM}", 'OIDC_CLAIMS_OPTIONS': { 'aud': { 'values': [ 'account', - OIDC_BACKEND_CLIENT_ID, - OIDC_FRONTEND_CLIENT_ID + KEYCLOAK_BACKEND_CLIENT_ID, + KEYCLOAK_FRONTEND_CLIENT_ID ], 'essential': True, }, 'iss': { 'values': [ - f"{OIDC_HOST}/realms/{OIDC_REALM}" + f"{KEYCLOAK_INTERNAL_SERVICE_URL}/realms/{KEYCLOAK_REALM}" ], 'essential': True, }, }, - 'USERINFO_ENDPOINT': f"{OIDC_HOST}/realms/{OIDC_REALM}/protocol/openid-connect/userinfo", + 'USERINFO_ENDPOINT': f"{KEYCLOAK_INTERNAL_SERVICE_URL}/realms/{KEYCLOAK_REALM}/protocol/openid-connect/userinfo", 'OIDC_CREATE_USER': True, 'OIDC_RESOLVE_USER_FUNCTION': 'api.oidc_utils.get_user_by_email', - 'OIDC_CLIENT_ID': OIDC_BACKEND_CLIENT_ID, - 'OIDC_CLIENT_SECRET': OIDC_BACKEND_CLIENT_SECRET, + 'OIDC_CLIENT_ID': KEYCLOAK_BACKEND_CLIENT_ID, + 'OIDC_CLIENT_SECRET': KEYCLOAK_BACKEND_CLIENT_SECRET, } diff --git a/medcat-trainer/webapp/frontend/public/config.template.json b/medcat-trainer/webapp/frontend/public/config.template.json index e028730f6..f869c2ae4 100644 --- a/medcat-trainer/webapp/frontend/public/config.template.json +++ b/medcat-trainer/webapp/frontend/public/config.template.json @@ -1,7 +1,9 @@ { - "USE_OIDC": "${VITE_USE_OIDC}", - "KEYCLOAK_URL": "${VITE_KEYCLOAK_URL}", - "KEYCLOAK_REALM": "${VITE_KEYCLOAK_REALM}", - "KEYCLOAK_CLIENT_ID": "${VITE_KEYCLOAK_CLIENT_ID}", - "LOGOUT_REDIRECT_URI": "${VITE_LOGOUT_REDIRECT_URI}" + "USE_OIDC": "${USE_OIDC}", + "KEYCLOAK_URL": "${KEYCLOAK_URL}", + "KEYCLOAK_REALM": "${KEYCLOAK_REALM}", + "KEYCLOAK_CLIENT_ID": "${KEYCLOAK_FRONTEND_CLIENT_ID}", + "KEYCLOAK_LOGOUT_REDIRECT_URI": "${KEYCLOAK_LOGOUT_REDIRECT_URI}", + "KEYCLOAK_TOKEN_MIN_VALIDITY": "${KEYCLOAK_TOKEN_MIN_VALIDITY}", + "KEYCLOAK_TOKEN_REFRESH_INTERVAL": "${KEYCLOAK_TOKEN_REFRESH_INTERVAL}" } diff --git a/medcat-trainer/webapp/frontend/src/App.vue b/medcat-trainer/webapp/frontend/src/App.vue index 73d89db0e..5b406daa5 100644 --- a/medcat-trainer/webapp/frontend/src/App.vue +++ b/medcat-trainer/webapp/frontend/src/App.vue @@ -104,7 +104,7 @@ export default { if (this.useOidc && this.$keycloak && this.$keycloak.authenticated) { this.$keycloak.logout({ - redirectUri: getRuntimeConfig().LOGOUT_REDIRECT_URI + redirectUri: getRuntimeConfig().KEYCLOAK_LOGOUT_REDIRECT_URI }) } else { if (this.$route.name !== 'home') { diff --git a/medcat-trainer/webapp/frontend/src/auth.ts b/medcat-trainer/webapp/frontend/src/auth.ts index e55eb77ea..834cc9bea 100644 --- a/medcat-trainer/webapp/frontend/src/auth.ts +++ b/medcat-trainer/webapp/frontend/src/auth.ts @@ -30,11 +30,11 @@ export const authPlugin = { // configure axios axios.defaults.headers.common['Authorization'] = `Bearer ${keycloak.token}` - const refreshInterval = Number(getRuntimeConfig().KEYCLOAK_TOKEN_REFRESH_INTERVAL) || 10000 - const minValidity = Number(getRuntimeConfig().KEYCLOAK_TOKEN_MIN_VALIDITY) || 30 + const refreshIntervalSecs = Number(getRuntimeConfig().KEYCLOAK_TOKEN_REFRESH_INTERVAL) + const minValiditySecs = Number(getRuntimeConfig().KEYCLOAK_TOKEN_MIN_VALIDITY) setInterval(() => { - keycloak.updateToken(minValidity) + keycloak.updateToken(minValiditySecs) .then(refreshed => { if (refreshed) { console.log('[AuthPlugin] Token refreshed') @@ -44,7 +44,7 @@ export const authPlugin = { .catch(err => { console.error('[AuthPlugin] Failed to refresh token', err) }) - }, refreshInterval) + }, (refreshIntervalSecs * 1000)) app.config.globalProperties.$keycloak = keycloak diff --git a/medcat-trainer/webapp/frontend/src/runtimeConfig.ts b/medcat-trainer/webapp/frontend/src/runtimeConfig.ts index 6acc7d016..c22ffb5d0 100644 --- a/medcat-trainer/webapp/frontend/src/runtimeConfig.ts +++ b/medcat-trainer/webapp/frontend/src/runtimeConfig.ts @@ -10,7 +10,7 @@ export interface RuntimeConfig { KEYCLOAK_URL: string KEYCLOAK_REALM: string KEYCLOAK_CLIENT_ID: string - LOGOUT_REDIRECT_URI: string + KEYCLOAK_LOGOUT_REDIRECT_URI: string KEYCLOAK_TOKEN_MIN_VALIDITY: number KEYCLOAK_TOKEN_REFRESH_INTERVAL: number } @@ -31,7 +31,7 @@ const DEFAULT_CONFIG: RuntimeConfig = { KEYCLOAK_URL: '', KEYCLOAK_REALM: '', KEYCLOAK_CLIENT_ID: '', - LOGOUT_REDIRECT_URI: '', + KEYCLOAK_LOGOUT_REDIRECT_URI: '', KEYCLOAK_TOKEN_MIN_VALIDITY: 0, KEYCLOAK_TOKEN_REFRESH_INTERVAL: 0 }; diff --git a/medcat-trainer/webapp/scripts/load_examples.py b/medcat-trainer/webapp/scripts/load_examples.py index f68433529..df2b04657 100644 --- a/medcat-trainer/webapp/scripts/load_examples.py +++ b/medcat-trainer/webapp/scripts/load_examples.py @@ -64,7 +64,7 @@ def main(port=8001, use_oidc = os.environ.get('USE_OIDC') logger.info('Checking for environment variable USE_OIDC...') - if use_oidc is not None and use_oidc in ('1', 'true', 't', 'y'): + if use_oidc is not None and use_oidc in '1': logger.info('Found environment variable USE_OIDC is set to truthy value. Will load data using JWT') token = get_keycloak_access_token() headers = { diff --git a/medcat-trainer/webapp/scripts/nginx-entrypoint.sh b/medcat-trainer/webapp/scripts/nginx-entrypoint.sh index fecfb0fe7..185f9eac4 100644 --- a/medcat-trainer/webapp/scripts/nginx-entrypoint.sh +++ b/medcat-trainer/webapp/scripts/nginx-entrypoint.sh @@ -3,40 +3,40 @@ set -e echo "Generating runtime config.json from template..." -# Set VITE_USE_OIDC to 0 if not provided (traditional auth mode) -export VITE_USE_OIDC="${VITE_USE_OIDC:-0}" +# Set USE_OIDC to 0 if not provided (traditional auth mode) +export USE_OIDC="${USE_OIDC:-0}" -# If OIDC is enabled, require all OIDC-related variables -if [ "$VITE_USE_OIDC" = "1" ]; then +# If OIDC is enabled, validate required variables +if [ "$USE_OIDC" = "1" ]; then echo "OIDC mode enabled - validating OIDC environment variables..." - if [ -z "$VITE_KEYCLOAK_URL" ]; then - echo "ERROR: VITE_KEYCLOAK_URL environment variable is required when VITE_USE_OIDC=1" + if [ -z "$KEYCLOAK_URL" ]; then + echo "ERROR: KEYCLOAK_URL is required when USE_OIDC=1" exit 1 fi - if [ -z "$VITE_KEYCLOAK_REALM" ]; then - echo "ERROR: VITE_KEYCLOAK_REALM environment variable is required when VITE_USE_OIDC=1" + if [ -z "$KEYCLOAK_REALM" ]; then + echo "ERROR: KEYCLOAK_REALM is required when USE_OIDC=1" exit 1 fi - if [ -z "$VITE_KEYCLOAK_CLIENT_ID" ]; then - echo "ERROR: VITE_KEYCLOAK_CLIENT_ID environment variable is required when VITE_USE_OIDC=1" + if [ -z "$KEYCLOAK_FRONTEND_CLIENT_ID" ]; then + echo "ERROR: KEYCLOAK_FRONTEND_CLIENT_ID is required when USE_OIDC=1" exit 1 fi - if [ -z "$VITE_LOGOUT_REDIRECT_URI" ]; then - echo "ERROR: VITE_LOGOUT_REDIRECT_URI environment variable is required when VITE_USE_OIDC=1" + if [ -z "$KEYCLOAK_LOGOUT_REDIRECT_URI" ]; then + echo "ERROR: KEYCLOAK_LOGOUT_REDIRECT_URI is required when USE_OIDC=1" exit 1 fi else - echo "Traditional auth mode enabled (VITE_USE_OIDC=0)" - # Traditional auth mode - set defaults for unused variables - export VITE_KEYCLOAK_URL="${VITE_KEYCLOAK_URL:-http://localhost}" - export VITE_KEYCLOAK_REALM="${VITE_KEYCLOAK_REALM:-default}" - export VITE_KEYCLOAK_CLIENT_ID="${VITE_KEYCLOAK_CLIENT_ID:-medcattrainer}" - export VITE_LOGOUT_REDIRECT_URI="${VITE_LOGOUT_REDIRECT_URI:-/}" + echo "Traditional auth mode enabled (USE_OIDC=0)" + # Set Defaults + export KEYCLOAK_URL="${KEYCLOAK_URL:-}" + export KEYCLOAK_REALM="${KEYCLOAK_REALM:-}" + export KEYCLOAK_LOGOUT_REDIRECT_URI="${KEYCLOAK_LOGOUT_REDIRECT_URI:-}" + export KEYCLOAK_FRONTEND_CLIENT_ID="${KEYCLOAK_FRONTEND_CLIENT_ID:-}" fi # Check if template exists @@ -46,6 +46,12 @@ if [ ! -f "$TEMPLATE_FILE" ]; then exit 1 fi +# Set token refresh settings with sensible defaults +# KEYCLOAK_TOKEN_MIN_VALIDITY: Refresh token if it expires in less than this many seconds (default: 30s) +# KEYCLOAK_TOKEN_REFRESH_INTERVAL: Check token validity every N seconds (default: 20s) +export KEYCLOAK_TOKEN_MIN_VALIDITY="${KEYCLOAK_TOKEN_MIN_VALIDITY:-30}" +export KEYCLOAK_TOKEN_REFRESH_INTERVAL="${KEYCLOAK_TOKEN_REFRESH_INTERVAL:-20}" + # Generate config.json from template envsubst < "$TEMPLATE_FILE" > /home/frontend/dist/config.json