Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions medcat-trainer/OIDC_AUTHENTICATION_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/"
}
```

Expand All @@ -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
Expand Down
20 changes: 8 additions & 12 deletions medcat-trainer/docker-compose-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
40 changes: 18 additions & 22 deletions medcat-trainer/docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
50 changes: 37 additions & 13 deletions medcat-trainer/webapp/api/core/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}


Expand Down
12 changes: 7 additions & 5 deletions medcat-trainer/webapp/frontend/public/config.template.json
Original file line number Diff line number Diff line change
@@ -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}"
}
2 changes: 1 addition & 1 deletion medcat-trainer/webapp/frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
8 changes: 4 additions & 4 deletions medcat-trainer/webapp/frontend/src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions medcat-trainer/webapp/frontend/src/runtimeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
};
Expand Down
2 changes: 1 addition & 1 deletion medcat-trainer/webapp/scripts/load_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
42 changes: 24 additions & 18 deletions medcat-trainer/webapp/scripts/nginx-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down