From a2dbec085bd0bf1ff83b0b96d3635e66bd644e64 Mon Sep 17 00:00:00 2001 From: Sid Kattoju Date: Mon, 3 Nov 2025 14:28:04 -0500 Subject: [PATCH 1/2] feat: add helm chart for keycloak --- keycloak/helm/.helmignore | 29 ++ keycloak/helm/Chart.yaml | 41 ++ keycloak/helm/README.md | 456 ++++++++++++++++++ keycloak/helm/examples/README.md | 226 +++++++++ .../helm/examples/ai-platform-values.yaml | 116 +++++ keycloak/helm/examples/dev-values.yaml | 76 +++ keycloak/helm/examples/minimal-values.yaml | 18 + keycloak/helm/examples/production-values.yaml | 129 +++++ .../with-existing-database-secret.yaml | 42 ++ keycloak/helm/templates/NOTES.txt | 117 +++++ keycloak/helm/templates/_helpers.tpl | 104 ++++ keycloak/helm/templates/keycloak-cr.yaml | 89 ++++ .../templates/keycloak-preflight-check.yaml | 106 ++++ .../helm/templates/keycloak-realm-cr.yaml | 88 ++++ keycloak/helm/templates/keycloak-secret.yaml | 15 + keycloak/helm/templates/rbac-preflight.yaml | 45 ++ keycloak/helm/templates/serviceaccount.yaml | 13 + keycloak/helm/values.yaml | 254 ++++++++++ 18 files changed, 1964 insertions(+) create mode 100644 keycloak/helm/.helmignore create mode 100644 keycloak/helm/Chart.yaml create mode 100644 keycloak/helm/README.md create mode 100644 keycloak/helm/examples/README.md create mode 100644 keycloak/helm/examples/ai-platform-values.yaml create mode 100644 keycloak/helm/examples/dev-values.yaml create mode 100644 keycloak/helm/examples/minimal-values.yaml create mode 100644 keycloak/helm/examples/production-values.yaml create mode 100644 keycloak/helm/examples/with-existing-database-secret.yaml create mode 100644 keycloak/helm/templates/NOTES.txt create mode 100644 keycloak/helm/templates/_helpers.tpl create mode 100644 keycloak/helm/templates/keycloak-cr.yaml create mode 100644 keycloak/helm/templates/keycloak-preflight-check.yaml create mode 100644 keycloak/helm/templates/keycloak-realm-cr.yaml create mode 100644 keycloak/helm/templates/keycloak-secret.yaml create mode 100644 keycloak/helm/templates/rbac-preflight.yaml create mode 100644 keycloak/helm/templates/serviceaccount.yaml create mode 100644 keycloak/helm/values.yaml diff --git a/keycloak/helm/.helmignore b/keycloak/helm/.helmignore new file mode 100644 index 0000000..b7a66cc --- /dev/null +++ b/keycloak/helm/.helmignore @@ -0,0 +1,29 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ +# Examples directory (optional - remove if you want to include examples in package) +examples/ +# Documentation +README.md +CONTRIBUTING.md + diff --git a/keycloak/helm/Chart.yaml b/keycloak/helm/Chart.yaml new file mode 100644 index 0000000..8c32651 --- /dev/null +++ b/keycloak/helm/Chart.yaml @@ -0,0 +1,41 @@ +apiVersion: v2 +name: keycloak +description: Red Hat Build of Keycloak deployment using the Keycloak Operator with realm and user management +type: application +version: 0.1.0 +appVersion: "25.0" + +keywords: + - keycloak + - authentication + - oidc + - oauth2 + - sso + - identity + - security + +home: https://github.com/rh-ai-quickstart/ai-architecture-charts +sources: + - https://github.com/rh-ai-quickstart/ai-architecture-charts + - https://www.keycloak.org/ + +maintainers: + - name: Red Hat AI Quickstart Team + email: ai-quickstart@redhat.com + +icon: https://www.keycloak.org/resources/images/keycloak_logo_480x108.png + +annotations: + category: Security + licenses: Apache-2.0 + operatorRequired: "true" + operatorName: "keycloak-operator" + operatorCRDs: "keycloaks.k8s.keycloak.org,keycloakrealmimports.k8s.keycloak.org" + +# Dependencies (if using pgvector as database) +# dependencies: +# - name: pgvector +# version: ">=0.1.0" +# repository: "https://rh-ai-quickstart.github.io/ai-architecture-charts" +# condition: database.enabled + diff --git a/keycloak/helm/README.md b/keycloak/helm/README.md new file mode 100644 index 0000000..e5b6e11 --- /dev/null +++ b/keycloak/helm/README.md @@ -0,0 +1,456 @@ +# Keycloak Helm Chart + +A Helm chart for deploying Red Hat Build of Keycloak on OpenShift/Kubernetes using the Keycloak Operator. This chart provides a production-ready Keycloak deployment with automated realm and user management. + +## Features + +✅ **Operator-based deployment** - Leverages Keycloak Operator for production-grade management +✅ **Automated realm creation** - Creates realms and clients via KeycloakRealmImport CRs +✅ **Default users** - Configurable default admin and test users +✅ **Database integration** - Supports PostgreSQL, MySQL, MariaDB, MSSQL, Oracle +✅ **OpenShift Route support** - Automatic route creation with TLS termination +✅ **Pre-flight checks** - Validates operator installation before deployment +✅ **Production-ready defaults** - Security, resource limits, and health checks configured +✅ **Highly configurable** - Extensive values for customization + +## Prerequisites + +### Required + +- **Kubernetes 1.24+** or **OpenShift 4.10+** +- **Helm 3.8+** +- **Keycloak Operator** installed (see [Installation](#operator-installation)) +- **Cluster admin permissions** for operator installation + +### Optional + +- PostgreSQL database (or use embedded H2 for testing only) + +## Operator Installation + +⚠️ **IMPORTANT**: The Keycloak Operator must be installed **before** deploying this chart. + +### OpenShift (Recommended) + +```bash +# Via OpenShift Console +# 1. Navigate to: Operators → OperatorHub +# 2. Search for: "Red Hat Build of Keycloak Operator" or "Keycloak Operator" +# 3. Click "Install" and follow the prompts + +# Or via CLI +oc create namespace keycloak-operator +cat </admin" +echo "Username: $ADMIN_USER" +echo "Password: $ADMIN_PASS" +``` + +## Configuration + +### Basic Configuration + +The chart comes with sensible defaults. Override values as needed: + +```bash +helm install keycloak ai-charts/keycloak \ + --namespace keycloak \ + --create-namespace \ + --set keycloak.replicas=2 \ + --set database.password=mySecurePassword +``` + +### Using a Values File + +Create a `my-values.yaml`: + +```yaml +keycloak: + replicas: 2 + +database: + host: postgresql.database.svc + name: keycloak + username: keycloak + password: changeme + +realm: + name: my-platform + displayName: "My AI Platform" + + client: + id: my-app + redirectUris: + - "https://my-app.example.com/*" + webOrigins: + - "https://my-app.example.com" + + users: + - username: admin + email: admin@example.com + firstName: Admin + lastName: User + enabled: true + emailVerified: true + credentials: + - type: password + value: AdminPassword123! + temporary: false + realmRoles: + - admin +``` + +Deploy with custom values: + +```bash +helm install keycloak ai-charts/keycloak \ + --namespace keycloak \ + --create-namespace \ + --values my-values.yaml +``` + +## Integration Examples + +### Using with AI Applications + +This chart is designed to integrate with AI architecture components: + +```yaml +# values.yaml +realm: + name: ai-platform + client: + id: ai-platform-client + redirectUris: + - "https://chatbot.example.com/*" + - "https://rag-app.example.com/*" + webOrigins: + - "https://chatbot.example.com" + - "https://rag-app.example.com" +``` + +### Using with pgvector Database + +Reference the pgvector chart from ai-architecture-charts: + +```yaml +# In your parent chart's Chart.yaml +dependencies: + - name: pgvector + version: "0.1.0" + repository: "https://rh-ai-quickstart.github.io/ai-architecture-charts" + - name: keycloak + version: "0.1.0" + repository: "https://rh-ai-quickstart.github.io/ai-architecture-charts" + +# In your parent chart's values.yaml +pgvector: + secret: + dbname: keycloak + user: keycloak + password: changeme + +keycloak: + database: + host: pgvector-postgresql + name: keycloak + username: keycloak + password: changeme +``` + +### Application Integration + +Configure your application to use Keycloak: + +```bash +# OIDC Discovery URL +KEYCLOAK_URL=https://keycloak.example.com +OIDC_DISCOVERY_URL=$KEYCLOAK_URL/realms/ai-platform/.well-known/openid-configuration + +# Client Configuration +CLIENT_ID=ai-platform-client +REDIRECT_URI=https://your-app.example.com/callback +``` + +## Configuration Reference + +### Key Values + +| Parameter | Description | Default | +|-----------|-------------|---------| +| `keycloak.replicas` | Number of Keycloak instances | `1` | +| `keycloak.resources.requests.memory` | Memory request | `512Mi` | +| `keycloak.resources.limits.memory` | Memory limit | `1Gi` | +| `database.enabled` | Use external database | `true` | +| `database.vendor` | Database type | `postgres` | +| `database.host` | Database hostname | `postgresql` | +| `database.port` | Database port | `5432` | +| `database.name` | Database name | `keycloak` | +| `database.username` | Database username | `keycloak` | +| `database.password` | Database password | `changeme` | +| `realm.enabled` | Create realm automatically | `true` | +| `realm.name` | Realm name | `ai-platform` | +| `realm.client.id` | Client ID | `ai-platform-client` | +| `preflight.enabled` | Run pre-flight checks | `true` | + +### Complete Values + +See [values.yaml](values.yaml) for all available configuration options. + +## Common Scenarios + +### Development Setup + +```yaml +# dev-values.yaml +keycloak: + replicas: 1 + resources: + requests: + memory: "256Mi" + cpu: "100m" + +database: + enabled: false # Use embedded H2 (not recommended for prod) + +realm: + users: + - username: dev + email: dev@example.com + credentials: + - type: password + value: dev123 + temporary: false +``` + +### Production Setup + +```yaml +# prod-values.yaml +keycloak: + replicas: 3 + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "2000m" + +database: + enabled: true + vendor: postgres + host: postgresql.production.svc + name: keycloak_prod + existingSecret: + enabled: true + name: keycloak-db-secret + usernameKey: db-user + passwordKey: db-password + +realm: + sslRequired: "all" + registrationAllowed: false + bruteForceProtected: true + + users: [] # Don't create default users in production +``` + +### Multiple Clients + +```yaml +# Use KeycloakRealmImport CR to add more clients +# This chart supports one client via values, for multiple clients +# create additional KeycloakRealmImport resources +``` + +## Troubleshooting + +### Operator Not Found + +```bash +# Check if operator is installed +kubectl get crd keycloaks.k8s.keycloak.org + +# If not found, install the operator first (see Prerequisites) +``` + +### Keycloak Pod Not Starting + +```bash +# Check Keycloak status +kubectl describe keycloak keycloak -n keycloak + +# Check pod logs +kubectl logs -l app=keycloak -n keycloak -f + +# Common issues: +# - Database connection failed (check credentials) +# - Insufficient resources (increase limits) +# - Image pull errors (check network/registry) +``` + +### Realm Not Importing + +```bash +# Check realm import status +kubectl describe keycloakrealmimport ai-platform -n keycloak + +# Ensure Keycloak is ready first +kubectl get keycloak keycloak -n keycloak + +# Delete and recreate if needed +kubectl delete keycloakrealmimport ai-platform -n keycloak +helm upgrade keycloak ai-charts/keycloak -n keycloak --reuse-values +``` + +### Route Not Created + +```bash +# Check if route is enabled +kubectl get route -n keycloak + +# On non-OpenShift, use port-forward instead +kubectl port-forward svc/keycloak-service 8080:8080 -n keycloak +``` + +## Upgrading + +```bash +# Update Helm repository +helm repo update + +# Upgrade deployment +helm upgrade keycloak ai-charts/keycloak \ + --namespace keycloak \ + --values my-values.yaml + +# Verify upgrade +kubectl get keycloak keycloak -n keycloak +``` + +## Uninstalling + +```bash +# Uninstall the chart +helm uninstall keycloak -n keycloak + +# Note: The operator and CRDs remain installed +# To remove operator (affects all Keycloak instances): +# kubectl delete -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.7/kubernetes/kubernetes.yml +``` + +## Security Considerations + +⚠️ **Important Security Notes** + +1. **Change default passwords** - Never use default passwords in production +2. **Use TLS** - Always enable TLS for production (`sslRequired: "all"`) +3. **Secure database** - Use strong database credentials and TLS +4. **Limit CORS** - Configure specific web origins, not wildcards +5. **Restrict redirects** - Use specific redirect URIs, not wildcards +6. **Enable brute force protection** - Already enabled by default +7. **Regular updates** - Keep Keycloak and operator updated +8. **Use secrets** - Store credentials in Kubernetes secrets, not values files +9. **RBAC** - Configure proper role-based access control +10. **Audit logs** - Enable and monitor Keycloak audit logs + +## Architecture + +This chart deploys: + +- **Keycloak CR** - Defines the Keycloak instance (managed by operator) +- **KeycloakRealmImport CR** - Defines realm, clients, and users +- **Secret** - Stores database credentials (if not using existing secret) +- **ServiceAccount** - For pre-flight checks and jobs +- **ClusterRole/Binding** - RBAC for pre-flight validation +- **Pre-flight Job** - Validates operator installation before deployment + +The Keycloak Operator creates: +- **StatefulSet** - Keycloak pods +- **Service** - Internal service +- **Route/Ingress** - External access (if enabled) +- **Admin Secret** - Initial admin credentials + +## Contributing + +This chart is part of the [ai-architecture-charts](https://github.com/rh-ai-quickstart/ai-architecture-charts) repository. + +To contribute: +1. Fork the repository +2. Make your changes +3. Test thoroughly +4. Submit a pull request + +## Resources + +- [Keycloak Documentation](https://www.keycloak.org/documentation) +- [Keycloak Operator Guide](https://www.keycloak.org/guides#operator) +- [Keycloak on OpenShift](https://www.keycloak.org/getting-started/getting-started-openshift) +- [AI Architecture Charts](https://github.com/rh-ai-quickstart/ai-architecture-charts) + +## License + +This chart is licensed under the Apache License 2.0. See LICENSE for details. + +Keycloak is licensed under the Apache License 2.0. + diff --git a/keycloak/helm/examples/README.md b/keycloak/helm/examples/README.md new file mode 100644 index 0000000..713c1df --- /dev/null +++ b/keycloak/helm/examples/README.md @@ -0,0 +1,226 @@ +# Keycloak Helm Chart Examples + +This directory contains example values files for common deployment scenarios. + +## Available Examples + +### 1. Development (`dev-values.yaml`) + +Quick setup for local development with relaxed security and simple passwords. + +**Features:** +- Single replica +- Embedded H2 database (no external DB needed) +- HTTP allowed (no TLS required) +- Self-registration enabled +- Simple passwords +- localhost redirect URIs + +**Usage:** +```bash +helm install keycloak ./helm --values examples/dev-values.yaml -n dev --create-namespace +``` + +**⚠️ WARNING:** Not suitable for production! Data is lost on pod restart. + +--- + +### 2. Production (`production-values.yaml`) + +Enterprise-grade configuration with high availability and strict security. + +**Features:** +- 3 replicas for HA +- External PostgreSQL database +- HTTPS required +- Strong security settings +- No default users +- Pod anti-affinity +- Monitoring enabled + +**Usage:** +```bash +# 1. Create database secret first +kubectl create secret generic keycloak-db-credentials \ + --from-literal=username=keycloak \ + --from-literal=password= \ + -n production + +# 2. Customize production-values.yaml with your URLs + +# 3. Deploy +helm install keycloak ./helm --values examples/production-values.yaml -n production --create-namespace +``` + +--- + +### 3. AI Platform (`ai-platform-values.yaml`) + +Optimized for AI/ML application authentication with role-based access. + +**Features:** +- AI-specific roles (data-scientist, ml-engineer, etc.) +- Multiple application redirect URIs +- Integration with pgvector database +- Longer session tokens for AI workflows +- Default users with role assignments + +**Usage:** +```bash +helm install keycloak ./helm --values examples/ai-platform-values.yaml -n ai-platform --create-namespace +``` + +--- + +### 4. Minimal (`minimal-values.yaml`) + +Bare minimum configuration using mostly chart defaults. + +**Features:** +- Minimal overrides +- Uses all chart defaults +- Only specifies database connection + +**Usage:** +```bash +helm install keycloak ./helm --values examples/minimal-values.yaml -n keycloak --create-namespace +``` + +--- + +### 5. Existing Database Secret (`with-existing-database-secret.yaml`) + +Use when database credentials are managed separately (e.g., by external secrets operator). + +**Features:** +- References existing Kubernetes secret +- No credential duplication +- Better secret management + +**Usage:** +```bash +# 1. Create secret (or use existing one) +kubectl create secret generic my-db-secret \ + --from-literal=db-username=keycloak \ + --from-literal=db-password=myPassword \ + -n keycloak + +# 2. Deploy +helm install keycloak ./helm --values examples/with-existing-database-secret.yaml -n keycloak --create-namespace +``` + +--- + +## Customization + +These examples are starting points. Customize them for your needs: + +```bash +# Copy an example +cp examples/production-values.yaml my-values.yaml + +# Edit for your environment +vim my-values.yaml + +# Deploy with your customizations +helm install keycloak ./helm --values my-values.yaml -n my-namespace --create-namespace +``` + +## Combining Values Files + +You can layer multiple values files: + +```bash +# Use production base + custom overrides +helm install keycloak ./helm \ + --values examples/production-values.yaml \ + --values my-custom-values.yaml \ + -n production --create-namespace +``` + +## Common Customizations + +### Change Database Connection + +```yaml +database: + host: my-postgres.example.com + port: 5432 + name: my_keycloak_db + username: my_user + password: my_password +``` + +### Add More Users + +```yaml +realm: + users: + - username: newuser + email: newuser@example.com + firstName: New + lastName: User + enabled: true + emailVerified: true + credentials: + - type: password + value: password123 + temporary: false + realmRoles: + - user +``` + +### Change Resource Limits + +```yaml +keycloak: + resources: + requests: + memory: "2Gi" + cpu: "1000m" + limits: + memory: "4Gi" + cpu: "4000m" +``` + +### Add Custom Realm Roles + +```yaml +realm: + roles: + realm: + - name: my-custom-role + description: My custom role description +``` + +## Testing Your Configuration + +Before deploying to production, test your configuration: + +```bash +# 1. Dry run to see generated manifests +helm install keycloak ./helm \ + --values my-values.yaml \ + --dry-run --debug + +# 2. Install in test namespace first +helm install keycloak ./helm \ + --values my-values.yaml \ + -n keycloak-test --create-namespace + +# 3. Verify deployment +kubectl get keycloak -n keycloak-test +kubectl get keycloakrealmimport -n keycloak-test + +# 4. Test login and functionality + +# 5. Clean up test +helm uninstall keycloak -n keycloak-test +``` + +## Need Help? + +- See main [README.md](../README.md) for detailed documentation +- Check [values.yaml](../values.yaml) for all available options +- Visit [Keycloak Documentation](https://www.keycloak.org/documentation) + diff --git a/keycloak/helm/examples/ai-platform-values.yaml b/keycloak/helm/examples/ai-platform-values.yaml new file mode 100644 index 0000000..a2c8dd2 --- /dev/null +++ b/keycloak/helm/examples/ai-platform-values.yaml @@ -0,0 +1,116 @@ +# AI Platform Integration Values +# Optimized for AI/ML application authentication +# +# Usage: +# helm install keycloak ./helm --values examples/ai-platform-values.yaml + +keycloak: + replicas: 2 + + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" + +# Integrate with pgvector database +database: + enabled: true + vendor: postgres + host: pgvector-postgresql # If using pgvector chart + port: 5432 + name: keycloak + username: keycloak + password: changeme # Use secret in production! + +realm: + name: ai-platform + displayName: "AI Platform" + + sslRequired: "external" + registrationAllowed: false + bruteForceProtected: true + + client: + id: ai-platform-client + name: "AI Platform Client" + description: "Authentication for AI/ML applications" + + redirectUris: + - "https://chatbot.example.com/*" + - "https://rag-app.example.com/*" + - "https://model-serving.example.com/*" + + webOrigins: + - "https://chatbot.example.com" + - "https://rag-app.example.com" + - "https://model-serving.example.com" + + # AI apps may need longer sessions + accessTokenLifespan: 600 # 10 minutes + + # AI platform roles + roles: + realm: + - name: admin + description: Platform administrator + - name: data-scientist + description: Can train and deploy models + - name: ml-engineer + description: Can manage ML infrastructure + - name: user + description: Can use AI applications + - name: viewer + description: Read-only access + + # Default users for AI platform + users: + - username: admin + email: admin@example.com + firstName: Platform + lastName: Admin + enabled: true + emailVerified: true + credentials: + - type: password + value: admin123 + temporary: true # Force password change on first login + realmRoles: + - admin + + - username: data-scientist + email: ds@example.com + firstName: Data + lastName: Scientist + enabled: true + emailVerified: true + credentials: + - type: password + value: ds123 + temporary: true + realmRoles: + - data-scientist + - user + + - username: testuser + email: user@example.com + firstName: Test + lastName: User + enabled: true + emailVerified: true + credentials: + - type: password + value: test123 + temporary: false + realmRoles: + - user + +preflight: + enabled: true + +commonLabels: + app: ai-platform + component: authentication + diff --git a/keycloak/helm/examples/dev-values.yaml b/keycloak/helm/examples/dev-values.yaml new file mode 100644 index 0000000..3101c75 --- /dev/null +++ b/keycloak/helm/examples/dev-values.yaml @@ -0,0 +1,76 @@ +# Development Environment Values +# Quick setup for local development and testing +# +# Usage: +# helm install keycloak ./helm --values examples/dev-values.yaml + +keycloak: + # Single instance for dev + replicas: 1 + + # Reduced resources for local testing + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" + +# Use embedded H2 database (WARNING: Not for production!) +# Data is lost when pod restarts +database: + enabled: false + +realm: + name: dev-realm + displayName: "Development Realm" + + # Relaxed security for dev + sslRequired: "none" + registrationAllowed: true # Allow self-registration in dev + + client: + id: dev-client + redirectUris: + - "http://localhost:3000/*" + - "http://localhost:5173/*" # Vite default port + - "http://localhost:8080/*" + webOrigins: + - "http://localhost:3000" + - "http://localhost:5173" + - "http://localhost:8080" + + # Development users with simple passwords + users: + - username: dev + email: dev@example.com + firstName: Developer + lastName: User + enabled: true + emailVerified: true + credentials: + - type: password + value: dev123 + temporary: false + realmRoles: + - admin + - user + + - username: test + email: test@example.com + firstName: Test + lastName: User + enabled: true + emailVerified: true + credentials: + - type: password + value: test123 + temporary: false + realmRoles: + - user + +# Enable preflight check even in dev +preflight: + enabled: true + diff --git a/keycloak/helm/examples/minimal-values.yaml b/keycloak/helm/examples/minimal-values.yaml new file mode 100644 index 0000000..3f5d252 --- /dev/null +++ b/keycloak/helm/examples/minimal-values.yaml @@ -0,0 +1,18 @@ +# Minimal Configuration Values +# Bare minimum setup with sensible defaults +# +# Usage: +# helm install keycloak ./helm --values examples/minimal-values.yaml + +# Everything else uses chart defaults +# This creates: +# - Single Keycloak instance +# - Realm named "ai-platform" +# - Client "ai-platform-client" +# - Admin and test users +# - PostgreSQL database integration + +database: + host: postgresql # Replace with your database service name + password: mySecurePassword # Replace with actual password + diff --git a/keycloak/helm/examples/production-values.yaml b/keycloak/helm/examples/production-values.yaml new file mode 100644 index 0000000..7ddebf2 --- /dev/null +++ b/keycloak/helm/examples/production-values.yaml @@ -0,0 +1,129 @@ +# Production Environment Values +# Enterprise-grade configuration with high availability +# +# Usage: +# helm install keycloak ./helm --values examples/production-values.yaml +# +# ⚠️ IMPORTANT: Replace all placeholder values before deploying! + +keycloak: + replicas: 3 # High availability with 3 replicas + + # Production resource allocation + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "2Gi" + cpu: "2000m" + + # Enable all monitoring features + health: + enabled: true + metrics: + enabled: true + + # Production-specific options + additionalOptions: + - name: log-level + value: INFO + - name: features + value: token-exchange,admin-fine-grained-authz + +# External PostgreSQL database (required for production) +database: + enabled: true + vendor: postgres + host: postgresql.database.svc.cluster.local # Replace with your database host + port: 5432 + name: keycloak_prod + + # Use existing secret for credentials (recommended) + existingSecret: + enabled: true + name: keycloak-db-credentials # Create this secret separately + usernameKey: username + passwordKey: password + + # OR set credentials directly (less secure) + # username: keycloak + # password: CHANGE-ME-TO-STRONG-PASSWORD + +realm: + name: production-realm + displayName: "Production Platform" + + # Strict security settings + sslRequired: "all" # Require HTTPS for all connections + registrationAllowed: false # Disable self-registration + resetPasswordAllowed: true # Allow password resets via email + bruteForceProtected: true # Enable brute force protection + + client: + id: production-client + name: "Production Application" + public: true + + # Specific redirect URIs (no wildcards in production!) + redirectUris: + - "https://app.example.com/auth/callback" + - "https://app.example.com/silent-check-sso.html" + + # Specific web origins (no wildcards!) + webOrigins: + - "https://app.example.com" + + # Shorter token lifespan for production + accessTokenLifespan: 300 # 5 minutes + + attributes: + "backchannel.logout.session.required": "true" + "post.logout.redirect.uris": "https://app.example.com/*" + + # Do NOT create default users in production + # Users should be managed via: + # - LDAP/Active Directory integration + # - SAML federation + # - Manual creation by administrators + users: [] + + # Define roles but no users + roles: + realm: + - name: admin + description: Administrator with full access + - name: user + description: Standard user + - name: viewer + description: Read-only access + +# Enable preflight checks +preflight: + enabled: true + +# Add common labels for production +commonLabels: + environment: production + team: platform + +# Add monitoring annotations +commonAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "8080" + prometheus.io/path: "/metrics" + +# Node affinity (optional - spread across availability zones) +affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/name + operator: In + values: + - keycloak + topologyKey: topology.kubernetes.io/zone + diff --git a/keycloak/helm/examples/with-existing-database-secret.yaml b/keycloak/helm/examples/with-existing-database-secret.yaml new file mode 100644 index 0000000..005cedf --- /dev/null +++ b/keycloak/helm/examples/with-existing-database-secret.yaml @@ -0,0 +1,42 @@ +# Using Existing Database Secret +# Use when database credentials are managed separately +# +# Prerequisites: +# 1. Create database secret first: +# kubectl create secret generic my-db-secret \ +# --from-literal=db-username=keycloak \ +# --from-literal=db-password=mySecurePassword +# +# Usage: +# helm install keycloak ./helm --values examples/with-existing-database-secret.yaml + +keycloak: + replicas: 2 + +database: + enabled: true + vendor: postgres + host: postgresql.database.svc + port: 5432 + name: keycloak + + # Reference existing secret instead of creating new one + existingSecret: + enabled: true + name: my-db-secret # Name of your existing secret + usernameKey: db-username # Key containing username + passwordKey: db-password # Key containing password + +realm: + name: my-realm + + client: + id: my-client + redirectUris: + - "https://app.example.com/*" + webOrigins: + - "https://app.example.com" + +preflight: + enabled: true + diff --git a/keycloak/helm/templates/NOTES.txt b/keycloak/helm/templates/NOTES.txt new file mode 100644 index 0000000..b5c02fd --- /dev/null +++ b/keycloak/helm/templates/NOTES.txt @@ -0,0 +1,117 @@ +🎉 Keycloak has been successfully deployed! + +=========================================== +📋 Deployment Information +=========================================== + +Namespace: {{ .Release.Namespace }} +Release Name: {{ .Release.Name }} +Keycloak Name: {{ include "keycloak.instanceName" . }} +{{- if .Values.realm.enabled }} +Realm: {{ .Values.realm.name }} +Client ID: {{ .Values.realm.client.id }} +{{- end }} + +=========================================== +🔍 Check Deployment Status +=========================================== + +# Check Keycloak instance status +kubectl get keycloak {{ include "keycloak.instanceName" . }} -n {{ .Release.Namespace }} + +{{- if .Values.realm.enabled }} +# Check realm import status +kubectl get keycloakrealmimport {{ .Values.realm.name }} -n {{ .Release.Namespace }} +{{- end }} + +# Check Keycloak pods +kubectl get pods -n {{ .Release.Namespace }} -l app=keycloak + +# Wait for Keycloak to be ready (may take 2-5 minutes) +kubectl wait --for=condition=Ready keycloak/{{ include "keycloak.instanceName" . }} -n {{ .Release.Namespace }} --timeout=300s + +=========================================== +🌐 Access Keycloak +=========================================== + +{{- if .Values.keycloak.route.enabled }} +# Get the Keycloak URL (OpenShift Route) +KEYCLOAK_URL=$(kubectl get route {{ include "keycloak.instanceName" . }}-ingress -n {{ .Release.Namespace }} -o jsonpath='{.spec.host}' 2>/dev/null) +if [ -n "$KEYCLOAK_URL" ]; then + echo "Keycloak URL: https://$KEYCLOAK_URL" + echo "Admin Console: https://$KEYCLOAK_URL/admin" +{{- if .Values.realm.enabled }} + echo "Realm URL: https://$KEYCLOAK_URL/realms/{{ .Values.realm.name }}" +{{- end }} +else + echo "⏳ Route not ready yet. Wait a moment and try again." +fi +{{- else }} +# Access via port-forward +kubectl port-forward svc/{{ include "keycloak.instanceName" . }}-service 8080:8080 -n {{ .Release.Namespace }} + +# Then open: http://localhost:8080 +{{- end }} + +=========================================== +🔑 Default Credentials +=========================================== + +{{- if .Values.realm.enabled }} +{{ if .Values.realm.users }} +Default users have been created in realm "{{ .Values.realm.name }}": +{{- range .Values.realm.users }} + +Username: {{ .username }} +Email: {{ .email }} +Password: {{ (index .credentials 0).value }} +{{- if .realmRoles }} +Roles: {{ .realmRoles | join ", " }} +{{- end }} +{{- end }} + +⚠️ SECURITY WARNING: Change these default passwords immediately in production! +{{- end }} +{{- end }} + +Admin Console Credentials: + Get from secret: kubectl get secret {{ include "keycloak.instanceName" . }}-initial-admin -n {{ .Release.Namespace }} -o jsonpath='{.data.username}' | base64 -d + Password: kubectl get secret {{ include "keycloak.instanceName" . }}-initial-admin -n {{ .Release.Namespace }} -o jsonpath='{.data.password}' | base64 -d + +=========================================== +📚 Next Steps +=========================================== + +1. Wait for Keycloak to be fully ready +2. Access the Admin Console and change default credentials +3. Configure your application to use: +{{- if .Values.realm.enabled }} + - OIDC Discovery URL: https:///realms/{{ .Values.realm.name }}/.well-known/openid-configuration + - Client ID: {{ .Values.realm.client.id }} + - Redirect URIs: Configure in your values file +{{- else }} + - Create a realm and client in the Admin Console +{{- end }} + +=========================================== +🛠️ Troubleshooting +=========================================== + +# View Keycloak logs +kubectl logs -l app=keycloak -n {{ .Release.Namespace }} -f + +# Check Keycloak events +kubectl get events -n {{ .Release.Namespace }} --sort-by='.lastTimestamp' + +# Describe Keycloak CR for status details +kubectl describe keycloak {{ include "keycloak.instanceName" . }} -n {{ .Release.Namespace }} + +{{- if .Values.realm.enabled }} +# Check realm import status +kubectl describe keycloakrealmimport {{ .Values.realm.name }} -n {{ .Release.Namespace }} +{{- end }} + +For more information, visit: + - Keycloak Documentation: https://www.keycloak.org/documentation + - Keycloak Operator: https://www.keycloak.org/guides#operator + diff --git a/keycloak/helm/templates/_helpers.tpl b/keycloak/helm/templates/_helpers.tpl new file mode 100644 index 0000000..9cd1ba5 --- /dev/null +++ b/keycloak/helm/templates/_helpers.tpl @@ -0,0 +1,104 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "keycloak.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "keycloak.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "keycloak.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "keycloak.labels" -}} +helm.sh/chart: {{ include "keycloak.chart" . }} +{{ include "keycloak.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- with .Values.commonLabels }} +{{ toYaml . }} +{{- end }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "keycloak.selectorLabels" -}} +app.kubernetes.io/name: {{ include "keycloak.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "keycloak.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "keycloak.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Keycloak instance name +*/}} +{{- define "keycloak.instanceName" -}} +{{- .Values.keycloak.name | default (include "keycloak.fullname" .) }} +{{- end }} + +{{/* +Database secret name +*/}} +{{- define "keycloak.databaseSecretName" -}} +{{- if .Values.database.existingSecret.enabled }} +{{- .Values.database.existingSecret.name }} +{{- else }} +{{- printf "%s-db-secret" (include "keycloak.fullname" .) }} +{{- end }} +{{- end }} + +{{/* +Database username key +*/}} +{{- define "keycloak.databaseUsernameKey" -}} +{{- if .Values.database.existingSecret.enabled }} +{{- .Values.database.existingSecret.usernameKey }} +{{- else }} +{{- "username" }} +{{- end }} +{{- end }} + +{{/* +Database password key +*/}} +{{- define "keycloak.databasePasswordKey" -}} +{{- if .Values.database.existingSecret.enabled }} +{{- .Values.database.existingSecret.passwordKey }} +{{- else }} +{{- "password" }} +{{- end }} +{{- end }} + diff --git a/keycloak/helm/templates/keycloak-cr.yaml b/keycloak/helm/templates/keycloak-cr.yaml new file mode 100644 index 0000000..356630f --- /dev/null +++ b/keycloak/helm/templates/keycloak-cr.yaml @@ -0,0 +1,89 @@ +--- +apiVersion: k8s.keycloak.org/v2alpha1 +kind: Keycloak +metadata: + name: {{ include "keycloak.instanceName" . }} + labels: + {{- include "keycloak.labels" . | nindent 4 }} + app.kubernetes.io/component: keycloak + {{- with .Values.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + instances: {{ .Values.keycloak.replicas }} + + {{- if and .Values.keycloak.image.repository .Values.keycloak.image.tag }} + image: {{ .Values.keycloak.image.repository }}:{{ .Values.keycloak.image.tag }} + {{- end }} + + {{- if .Values.database.enabled }} + # Use external database for persistence + db: + vendor: {{ .Values.database.vendor }} + host: {{ .Values.database.host }} + port: {{ .Values.database.port }} + database: {{ .Values.database.name }} + usernameSecret: + name: {{ include "keycloak.databaseSecretName" . }} + key: {{ include "keycloak.databaseUsernameKey" . }} + passwordSecret: + name: {{ include "keycloak.databaseSecretName" . }} + key: {{ include "keycloak.databasePasswordKey" . }} + {{- end }} + + # HTTP settings + http: + httpEnabled: {{ .Values.keycloak.http.enabled }} + httpPort: {{ .Values.keycloak.http.port }} + + # Hostname configuration + hostname: + strict: {{ .Values.keycloak.hostname.strict }} + strictBackchannel: {{ .Values.keycloak.hostname.strictBackchannel }} + + {{- if .Values.keycloak.route.enabled }} + # Ingress/Route configuration + ingress: + enabled: true + {{- with .Values.keycloak.route.annotations }} + annotations: + {{- toYaml . | nindent 6 }} + {{- end }} + {{- end }} + + # Resource limits + resources: + requests: + {{- toYaml .Values.keycloak.resources.requests | nindent 6 }} + limits: + {{- toYaml .Values.keycloak.resources.limits | nindent 6 }} + + # Additional configuration + additionalOptions: + {{- if .Values.keycloak.health.enabled }} + - name: health-enabled + value: "true" + {{- end }} + {{- if .Values.keycloak.metrics.enabled }} + - name: metrics-enabled + value: "true" + {{- end }} + {{- if .Values.keycloak.http.enabled }} + - name: http-enabled + value: "true" + {{- end }} + {{- if .Values.keycloak.proxy.headers }} + # Proxy configuration (for routes with TLS termination) + - name: proxy-headers + value: {{ .Values.keycloak.proxy.headers | quote }} + {{- end }} + - name: hostname-strict + value: {{ .Values.keycloak.hostname.strict | quote }} + - name: hostname-strict-backchannel + value: {{ .Values.keycloak.hostname.strictBackchannel | quote }} + {{- range .Values.keycloak.additionalOptions }} + - name: {{ .name }} + value: {{ .value | quote }} + {{- end }} + diff --git a/keycloak/helm/templates/keycloak-preflight-check.yaml b/keycloak/helm/templates/keycloak-preflight-check.yaml new file mode 100644 index 0000000..423dccb --- /dev/null +++ b/keycloak/helm/templates/keycloak-preflight-check.yaml @@ -0,0 +1,106 @@ +{{- if .Values.preflight.enabled }} +--- +# Pre-flight check to ensure Keycloak Operator is installed +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "keycloak.fullname" . }}-preflight + labels: + {{- include "keycloak.labels" . | nindent 4 }} + app.kubernetes.io/component: preflight + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-5" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +spec: + ttlSecondsAfterFinished: 60 + backoffLimit: 1 + template: + metadata: + labels: + {{- include "keycloak.selectorLabels" . | nindent 8 }} + app.kubernetes.io/component: preflight + spec: + restartPolicy: Never + serviceAccountName: {{ include "keycloak.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: check-operator + image: "{{ .Values.preflight.image.repository }}:{{ .Values.preflight.image.tag }}" + imagePullPolicy: {{ .Values.global.imagePullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 10 }} + resources: + {{- toYaml .Values.preflight.resources | nindent 10 }} + command: + - /bin/bash + - -c + - | + set -e + + echo "==========================================" + echo "🔍 Keycloak Operator Pre-flight Check" + echo "==========================================" + echo "" + + # Check if Keycloak CRD exists (indicates operator is installed) + if kubectl get crd keycloaks.k8s.keycloak.org &>/dev/null; then + echo "✅ Keycloak CRD found: keycloaks.k8s.keycloak.org" + else + echo "❌ ERROR: Keycloak Operator is NOT installed!" + echo "" + echo "The Keycloak Operator must be installed before deploying with Keycloak." + echo "" + echo "📋 Installation Instructions:" + echo " OpenShift:" + echo " 1. Open OpenShift Console" + echo " 2. Navigate to: Operators → OperatorHub" + echo " 3. Search for: 'Red Hat Build of Keycloak Operator' or 'Keycloak Operator'" + echo " 4. Click Install and follow the prompts" + echo "" + echo " Kubernetes:" + echo " kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.7/kubernetes/kubernetes.yml" + echo "" + exit 1 + fi + + # Check if RealmImport CRD exists + if kubectl get crd keycloakrealmimports.k8s.keycloak.org &>/dev/null; then + echo "✅ KeycloakRealmImport CRD found: keycloakrealmimports.k8s.keycloak.org" + else + echo "⚠️ WARNING: KeycloakRealmImport CRD not found" + echo " Realm auto-import may not work." + fi + + # Check if operator pod is running + echo "" + echo "🔍 Checking for running operator pods..." + OPERATOR_PODS=$(kubectl get pods --all-namespaces -l app.kubernetes.io/name=keycloak-operator -o name 2>/dev/null || echo "") + + if [ -z "$OPERATOR_PODS" ]; then + # Try alternative label + OPERATOR_PODS=$(kubectl get pods --all-namespaces -l name=keycloak-operator -o name 2>/dev/null || echo "") + fi + + if [ -z "$OPERATOR_PODS" ]; then + # Try RHBK operator + OPERATOR_PODS=$(kubectl get pods --all-namespaces -l app.kubernetes.io/name=rhbk-operator -o name 2>/dev/null || echo "") + fi + + if [ -n "$OPERATOR_PODS" ]; then + echo "✅ Keycloak operator pod(s) found and running" + echo "$OPERATOR_PODS" + else + echo "⚠️ WARNING: Could not find running Keycloak operator pods" + echo " The operator may still be starting up." + echo " If deployment fails, check operator status." + fi + + echo "" + echo "==========================================" + echo "✅ Pre-flight check PASSED" + echo " Proceeding with Keycloak deployment..." + echo "==========================================" +{{- end }} + diff --git a/keycloak/helm/templates/keycloak-realm-cr.yaml b/keycloak/helm/templates/keycloak-realm-cr.yaml new file mode 100644 index 0000000..c57652c --- /dev/null +++ b/keycloak/helm/templates/keycloak-realm-cr.yaml @@ -0,0 +1,88 @@ +{{- if .Values.realm.enabled }} +--- +apiVersion: k8s.keycloak.org/v2alpha1 +kind: KeycloakRealmImport +metadata: + name: {{ .Values.realm.name }} + labels: + {{- include "keycloak.labels" . | nindent 4 }} + app.kubernetes.io/component: realm + {{- with .Values.commonAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + keycloakCRName: {{ include "keycloak.instanceName" . }} + realm: + id: {{ .Values.realm.name }} + realm: {{ .Values.realm.name }} + enabled: true + displayName: {{ .Values.realm.displayName | quote }} + sslRequired: {{ .Values.realm.sslRequired | quote }} + registrationAllowed: {{ .Values.realm.registrationAllowed }} + loginWithEmailAllowed: {{ .Values.realm.loginWithEmailAllowed }} + duplicateEmailsAllowed: {{ .Values.realm.duplicateEmailsAllowed }} + resetPasswordAllowed: {{ .Values.realm.resetPasswordAllowed }} + editUsernameAllowed: {{ .Values.realm.editUsernameAllowed }} + bruteForceProtected: {{ .Values.realm.bruteForceProtected }} + + {{- if .Values.realm.roles }} + # Realm roles + roles: + {{- if .Values.realm.roles.realm }} + realm: + {{- toYaml .Values.realm.roles.realm | nindent 8 }} + {{- end }} + {{- if .Values.realm.roles.client }} + client: + {{- toYaml .Values.realm.roles.client | nindent 8 }} + {{- end }} + {{- end }} + + # Client configuration + clients: + - clientId: {{ .Values.realm.client.id }} + name: {{ .Values.realm.client.name | quote }} + description: {{ .Values.realm.client.description | quote }} + enabled: true + clientAuthenticatorType: client-secret + redirectUris: + {{- toYaml .Values.realm.client.redirectUris | nindent 10 }} + webOrigins: + {{- toYaml .Values.realm.client.webOrigins | nindent 10 }} + standardFlowEnabled: {{ .Values.realm.client.standardFlowEnabled }} + implicitFlowEnabled: {{ .Values.realm.client.implicitFlowEnabled }} + directAccessGrantsEnabled: {{ .Values.realm.client.directAccessGrantsEnabled }} + serviceAccountsEnabled: {{ .Values.realm.client.serviceAccountsEnabled }} + publicClient: {{ .Values.realm.client.public }} + protocol: openid-connect + attributes: + "access.token.lifespan": {{ .Values.realm.client.accessTokenLifespan | quote }} + {{- range $key, $value := .Values.realm.client.attributes }} + {{ $key | quote }}: {{ $value | quote }} + {{- end }} + + {{- if .Values.realm.users }} + # Default users + users: + {{- range .Values.realm.users }} + - username: {{ .username }} + email: {{ .email }} + firstName: {{ .firstName }} + lastName: {{ .lastName }} + enabled: {{ .enabled }} + emailVerified: {{ .emailVerified }} + credentials: + {{- toYaml .credentials | nindent 10 }} + {{- if .realmRoles }} + realmRoles: + {{- toYaml .realmRoles | nindent 10 }} + {{- end }} + {{- if .clientRoles }} + clientRoles: + {{- toYaml .clientRoles | nindent 10 }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} + diff --git a/keycloak/helm/templates/keycloak-secret.yaml b/keycloak/helm/templates/keycloak-secret.yaml new file mode 100644 index 0000000..4a0389f --- /dev/null +++ b/keycloak/helm/templates/keycloak-secret.yaml @@ -0,0 +1,15 @@ +{{- if and .Values.database.enabled (not .Values.database.existingSecret.enabled) }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "keycloak.databaseSecretName" . }} + labels: + {{- include "keycloak.labels" . | nindent 4 }} + app.kubernetes.io/component: database +type: Opaque +stringData: + username: {{ .Values.database.username | quote }} + password: {{ .Values.database.password | quote }} +{{- end }} + diff --git a/keycloak/helm/templates/rbac-preflight.yaml b/keycloak/helm/templates/rbac-preflight.yaml new file mode 100644 index 0000000..5a0e744 --- /dev/null +++ b/keycloak/helm/templates/rbac-preflight.yaml @@ -0,0 +1,45 @@ +{{- if .Values.preflight.enabled }} +--- +# ClusterRole for preflight check - needs to list CRDs +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: {{ include "keycloak.fullname" . }}-preflight + labels: + {{- include "keycloak.labels" . | nindent 4 }} + app.kubernetes.io/component: preflight + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +rules: + - apiGroups: ["apiextensions.k8s.io"] + resources: ["customresourcedefinitions"] + verbs: ["get", "list"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["get", "list"] + +--- +# ClusterRoleBinding for preflight check +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: {{ include "keycloak.fullname" . }}-preflight + labels: + {{- include "keycloak.labels" . | nindent 4 }} + app.kubernetes.io/component: preflight + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "-10" + "helm.sh/hook-delete-policy": hook-succeeded,before-hook-creation +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: {{ include "keycloak.fullname" . }}-preflight +subjects: + - kind: ServiceAccount + name: {{ include "keycloak.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} + diff --git a/keycloak/helm/templates/serviceaccount.yaml b/keycloak/helm/templates/serviceaccount.yaml new file mode 100644 index 0000000..13867c2 --- /dev/null +++ b/keycloak/helm/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "keycloak.serviceAccountName" . }} + labels: + {{- include "keycloak.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} + diff --git a/keycloak/helm/values.yaml b/keycloak/helm/values.yaml new file mode 100644 index 0000000..7e7153a --- /dev/null +++ b/keycloak/helm/values.yaml @@ -0,0 +1,254 @@ +# Default values for Keycloak chart +# This chart deploys Keycloak using the Keycloak Operator + +global: + # Global image settings (override if needed) + imageRegistry: quay.io + imagePullPolicy: IfNotPresent + +# ⚠️ PREREQUISITE: Keycloak Operator must be installed by cluster admin +# Installation: +# OpenShift: Operators → OperatorHub → Search "Keycloak Operator" +# CLI: kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.0.7/kubernetes/kubernetes.yml +# +# The chart includes a pre-flight check that will FAIL if operator is not installed + +# Keycloak instance configuration +keycloak: + # Name of the Keycloak instance + name: keycloak + + # Number of Keycloak replicas + replicas: 1 + + # Keycloak container image (leave empty to use operator default) + image: + repository: "" # e.g., "quay.io/keycloak/keycloak" + tag: "" # e.g., "25.0" + + # Resource limits and requests + resources: + requests: + memory: "512Mi" + cpu: "250m" + limits: + memory: "1Gi" + cpu: "1000m" + + # Enable health and metrics endpoints + health: + enabled: true + + metrics: + enabled: true + + # Proxy configuration (for OpenShift routes with TLS termination) + proxy: + headers: xforwarded + + # Hostname configuration + hostname: + strict: false + strictBackchannel: false + + # HTTP configuration + http: + enabled: true + port: 8080 + + # OpenShift Route configuration (operator will create route automatically) + route: + enabled: true + # annotations: {} + # host: "" # Leave empty for auto-generated hostname + + # Additional Keycloak options (passed to server) + additionalOptions: [] + # - name: log-level + # value: INFO + # - name: features + # value: token-exchange,admin-fine-grained-authz + +# Database configuration +database: + # Use external database (recommended for production) + enabled: true + + # Database vendor (postgres, mysql, mariadb, mssql, oracle) + vendor: postgres + + # Database connection details + host: postgresql # Service name if using in-cluster database + port: 5432 + name: keycloak + + # Database credentials (stored in secret) + username: keycloak + password: changeme # ⚠️ Change in production! + + # Existing secret containing database credentials + # If provided, username/password above are ignored + existingSecret: + enabled: false + name: "" + usernameKey: username + passwordKey: password + +# Realm configuration +realm: + # Create a realm via KeycloakRealmImport CR + enabled: true + + # Realm name + name: "ai-platform" + + # Realm display name + displayName: "AI Platform" + + # Realm settings + sslRequired: "external" # none, external, all + registrationAllowed: false + loginWithEmailAllowed: true + duplicateEmailsAllowed: false + resetPasswordAllowed: true + editUsernameAllowed: false + bruteForceProtected: true + + # Client configuration + client: + # Client ID + id: "ai-platform-client" + + # Client name and description + name: "AI Platform Client" + description: "Default client for AI platform applications" + + # Client type (public = frontend apps, confidential = backend services) + public: true + + # OAuth/OIDC settings + standardFlowEnabled: true # Authorization code flow + implicitFlowEnabled: false # Implicit flow (not recommended) + directAccessGrantsEnabled: true # Direct access (password grant) + serviceAccountsEnabled: false # Service account for confidential clients + + # Valid redirect URIs (wildcards allowed) + redirectUris: + - "*" + + # Web origins for CORS + webOrigins: + - "*" + + # Token lifespan (seconds) + accessTokenLifespan: 300 # 5 minutes + + # Additional client attributes + attributes: {} + # post.logout.redirect.uris: "*" + # backchannel.logout.session.required: "true" + + # Default users to create in the realm + users: + # Admin user + - username: admin + email: admin@example.com + firstName: Admin + lastName: User + enabled: true + emailVerified: true + # Password (should be changed on first login in production) + credentials: + - type: password + value: admin123 + temporary: false + # Realm roles + realmRoles: + - admin + - user + # Client roles (optional) + # clientRoles: + # ai-platform-client: + # - client-admin + + # Test user + - username: testuser + email: testuser@example.com + firstName: Test + lastName: User + enabled: true + emailVerified: true + credentials: + - type: password + value: test123 + temporary: false + realmRoles: + - user + + # Realm roles + roles: + realm: + - name: admin + description: Administrator role with full access + - name: user + description: Standard user role + # Client roles (optional) + # client: + # ai-platform-client: + # - name: client-admin + # description: Client administrator + +# Pre-flight check configuration +preflight: + # Enable pre-flight check to verify operator is installed + enabled: true + + # Image for preflight check job + image: + repository: quay.io/openshift/origin-cli + tag: latest + + # Resource limits for preflight job + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "128Mi" + cpu: "200m" + +# ServiceAccount +serviceAccount: + create: true + annotations: {} + name: "" + +# Pod Security Context +podSecurityContext: {} + +# Security Context +securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + readOnlyRootFilesystem: false + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +# Node selector +nodeSelector: {} + +# Tolerations +tolerations: [] + +# Affinity +affinity: {} + +# Common labels to add to all resources +commonLabels: {} + +# Common annotations to add to all resources +commonAnnotations: {} + From f4781f317d620575dac43e3d9cc8adb1384a77a3 Mon Sep 17 00:00:00 2001 From: Sid Kattoju Date: Mon, 3 Nov 2025 18:12:20 -0500 Subject: [PATCH 2/2] added readme and detailed integration guide with examples --- keycloak/helm/INTEGRATION_GUIDE.md | 1508 ++++++++++++++++++++++++++++ keycloak/helm/README.md | 154 +++ 2 files changed, 1662 insertions(+) create mode 100644 keycloak/helm/INTEGRATION_GUIDE.md diff --git a/keycloak/helm/INTEGRATION_GUIDE.md b/keycloak/helm/INTEGRATION_GUIDE.md new file mode 100644 index 0000000..dc24d53 --- /dev/null +++ b/keycloak/helm/INTEGRATION_GUIDE.md @@ -0,0 +1,1508 @@ +# Keycloak Integration Guide for Applications + +A comprehensive guide for integrating Keycloak authentication in your applications, using the Spending Transaction Monitor as a reference implementation. + +## Table of Contents + +- [Architecture Overview](#architecture-overview) +- [Authentication Flow](#authentication-flow) +- [Frontend Integration (React)](#frontend-integration-react) +- [Backend Integration (Python/FastAPI)](#backend-integration-python-fastapi) +- [Configuration](#configuration) +- [Protected Resources](#protected-resources) +- [User Profile Management](#user-profile-management) +- [User ID Mapping](#user-id-mapping-critical) +- [Role-Based Access Control](#role-based-access-control) +- [User Registration](#user-registration) +- [Best Practices](#best-practices) +- [Troubleshooting](#troubleshooting) + +--- + +## Architecture Overview + +``` +┌─────────────┐ ┌──────────────┐ ┌─────────────┐ +│ Browser │◄───────►│ Keycloak │ │ Backend │ +│ (React) │ OAuth2 │ Server │ │ (FastAPI) │ +└──────┬──────┘ OIDC └──────┬───────┘ └──────┬──────┘ + │ │ │ + │ 1. Login Redirect │ │ + ├───────────────────────►│ │ + │ │ │ + │ 2. Auth Code │ │ + │◄───────────────────────┤ │ + │ │ │ + │ 3. Exchange for Token │ │ + ├───────────────────────►│ │ + │ │ │ + │ 4. Access Token + ID Token │ + │◄───────────────────────┤ │ + │ │ │ + │ 5. API Request + Bearer Token │ + ├────────────────────────────────────────────────►│ + │ │ │ + │ │ 6. Validate Token │ + │ │◄───────────────────────┤ + │ │ (JWKS) │ + │ │ │ + │ │ 7. Return JWKS │ + │ ├───────────────────────►│ + │ │ │ + │ 8. API Response (with user data) │ + │◄────────────────────────────────────────────────┤ + │ │ │ +``` + +### Key Components + +1. **Keycloak Server**: Identity Provider (IdP) and Authorization Server +2. **Frontend (React)**: Public client using Authorization Code Flow with PKCE +3. **Backend (FastAPI)**: Resource server validating JWT tokens +4. **Database**: User data storage with Keycloak ID mapping + +--- + +## Authentication Flow + +### Detailed Sequence Diagram + +```mermaid +sequenceDiagram + participant User + participant React as React App + participant KC as Keycloak + participant API as FastAPI Backend + participant DB as Database + + Note over User,DB: 1. Initial Authentication + User->>React: Access protected route + React->>React: Check auth state + React->>User: Redirect to login + User->>KC: Navigate to Keycloak login + User->>KC: Enter credentials + KC->>KC: Validate credentials + KC->>React: Redirect with auth code + React->>KC: Exchange code for tokens (PKCE) + KC->>React: Return access_token + id_token + refresh_token + React->>React: Store tokens (memory/localStorage) + React->>React: Update auth state + + Note over User,DB: 2. Accessing Protected Resources + User->>React: Click on feature + React->>API: GET /users/profile
Authorization: Bearer {access_token} + API->>API: Extract Bearer token + API->>KC: GET /.well-known/openid-configuration + KC->>API: Return OIDC configuration + API->>KC: GET /protocol/openid-connect/certs (JWKS) + KC->>API: Return public keys + API->>API: Validate JWT signature
Verify issuer, audience, expiry + API->>API: Extract claims (sub, email, roles) + API->>DB: SELECT * FROM users WHERE keycloak_id = {sub} + DB->>API: Return user record + API->>API: Attach user context to request + API->>API: Check authorization (roles) + API->>DB: Execute business logic query + DB->>API: Return data + API->>React: 200 OK {user_profile} + React->>User: Display profile + + Note over User,DB: 3. Token Refresh + React->>React: Token near expiry + React->>KC: POST /token
grant_type=refresh_token + KC->>React: New access_token + React->>React: Update stored token + + Note over User,DB: 4. Logout + User->>React: Click logout + React->>KC: GET /logout + KC->>KC: Invalidate session + KC->>React: Redirect to post_logout_redirect_uri + React->>React: Clear tokens and state + React->>User: Show logged out state +``` + +--- + +## Frontend Integration (React) + +### Dependencies + +```json +{ + "dependencies": { + "react": "^19.1.1", + "react-oidc-context": "^3.3.0" + } +} +``` + +### Installation + +```bash +npm install react-oidc-context +# or +pnpm add react-oidc-context +``` + +### Configuration + +```typescript +// src/config/auth.ts +export interface AuthConfig { + environment: 'development' | 'production'; + bypassAuth: boolean; + keycloak: { + authority: string; // Keycloak realm URL + clientId: string; // Client ID from Keycloak + redirectUri: string; // Your app's callback URL + postLogoutRedirectUri: string; // Where to go after logout + }; +} + +export const authConfig: AuthConfig = { + environment: import.meta.env.VITE_ENVIRONMENT || 'development', + bypassAuth: import.meta.env.VITE_BYPASS_AUTH === 'true', + keycloak: { + // Auto-discovery URL format: {baseUrl}/realms/{realm-name} + authority: import.meta.env.VITE_KEYCLOAK_URL || + 'https://keycloak.example.com/realms/ai-platform', + clientId: import.meta.env.VITE_KEYCLOAK_CLIENT_ID || + 'ai-platform-client', + redirectUri: import.meta.env.VITE_KEYCLOAK_REDIRECT_URI || + window.location.origin, + postLogoutRedirectUri: import.meta.env.VITE_KEYCLOAK_POST_LOGOUT_REDIRECT_URI || + window.location.origin, + }, +}; +``` + +### Auth Provider Setup + +```typescript +// src/contexts/AuthContext.tsx +import React, { createContext, useState, useEffect, useCallback, useMemo } from 'react'; +import { + AuthProvider as OIDCProvider, + useAuth as useOIDCAuth, +} from 'react-oidc-context'; +import { authConfig } from '../config/auth'; + +export const AuthContext = createContext(undefined); + +/** + * Production OIDC Auth Provider + */ +export const AuthProvider = ({ children }: { children: React.ReactNode }) => { + const oidcConfig = useMemo( + () => ({ + authority: authConfig.keycloak.authority, + client_id: authConfig.keycloak.clientId, + redirect_uri: authConfig.keycloak.redirectUri, + post_logout_redirect_uri: authConfig.keycloak.postLogoutRedirectUri, + + // OAuth 2.0 settings + response_type: 'code', // Authorization Code Flow + scope: 'openid profile email', // Requested scopes + + // Token management + automaticSilentRenew: true, // Auto-refresh tokens + loadUserInfo: false, // Use ID token claims instead + monitorSession: true, // Monitor Keycloak session + + // Storage + storeUser: true, // Persist user in localStorage + }), + [], + ); + + return ( + + {children} + + ); +}; + +/** + * Wrapper to adapt OIDC provider to custom auth context + */ +const OIDCAuthWrapper = ({ children }: { children: React.ReactNode }) => { + const oidcAuth = useOIDCAuth(); + const [user, setUser] = useState(null); + + useEffect(() => { + if (oidcAuth.error) { + console.error('OIDC Authentication Error:', oidcAuth.error); + } + + if (oidcAuth.user) { + // Extract user info from token claims + const newUser: User = { + id: oidcAuth.user.profile.sub!, // Subject (Keycloak user ID) + email: oidcAuth.user.profile.email!, + username: oidcAuth.user.profile.preferred_username, + name: oidcAuth.user.profile.name, + // Extract realm roles from token + roles: oidcAuth.user.profile.realm_access?.roles || ['user'], + }; + setUser(newUser); + + // Pass access token to API client + if (oidcAuth.user.access_token) { + ApiClient.setToken(oidcAuth.user.access_token); + } + } else { + setUser(null); + ApiClient.setToken(null); + } + }, [oidcAuth.user, oidcAuth.error]); + + const login = useCallback(() => oidcAuth.signinRedirect(), [oidcAuth]); + const logout = useCallback(() => oidcAuth.signoutRedirect(), [oidcAuth]); + + const contextValue: AuthContextType = { + user, + isAuthenticated: !!oidcAuth.user, + isLoading: oidcAuth.isLoading, + login, + logout, + error: oidcAuth.error ? new Error(oidcAuth.error.message) : null, + }; + + return {children}; +}; +``` + +### API Client with Token Management + +```typescript +// src/services/apiClient.ts +export class ApiClient { + private static token: string | null = null; + private static baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'; + + static setToken(token: string | null) { + this.token = token; + } + + static async request(endpoint: string, options: RequestInit = {}): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + // Add Bearer token if available + if (this.token) { + headers['Authorization'] = `Bearer ${this.token}`; + } + + const response = await fetch(`${this.baseURL}${endpoint}`, { + ...options, + headers, + }); + + if (response.status === 401) { + // Token expired or invalid - trigger re-authentication + // This will be caught by the auth provider + throw new Error('Unauthorized'); + } + + if (!response.ok) { + throw new Error(`API Error: ${response.statusText}`); + } + + return response.json(); + } + + static get(endpoint: string): Promise { + return this.request(endpoint, { method: 'GET' }); + } + + static post(endpoint: string, data: unknown): Promise { + return this.request(endpoint, { + method: 'POST', + body: JSON.stringify(data), + }); + } +} +``` + +### Protecting Routes + +```typescript +// src/components/ProtectedRoute.tsx +import { useAuth } from '../hooks/useAuth'; +import { Navigate } from '@tanstack/react-router'; + +export function ProtectedRoute({ children }: { children: React.ReactNode }) { + const { isAuthenticated, isLoading } = useAuth(); + + if (isLoading) { + return
Loading...
; + } + + if (!isAuthenticated) { + return ; + } + + return <>{children}; +} +``` + +### Using Authentication in Components + +```typescript +// src/pages/Dashboard.tsx +import { useAuth } from '../hooks/useAuth'; + +export function Dashboard() { + const { user, isAuthenticated, logout } = useAuth(); + + if (!isAuthenticated) { + return ; + } + + return ( +
+

Welcome, {user?.name}

+

Email: {user?.email}

+

Roles: {user?.roles.join(', ')}

+ +
+ ); +} +``` + +--- + +## Backend Integration (Python/FastAPI) + +### Dependencies + +```toml +# pyproject.toml +[project] +dependencies = [ + "fastapi>=0.104.0", + "python-jose[cryptography]>=3.3.0", # JWT validation + "httpx>=0.25.0", # HTTP client for Keycloak + "sqlalchemy>=2.0.0", # Database ORM +] +``` + +### Installation + +```bash +pip install fastapi python-jose[cryptography] httpx sqlalchemy +# or +uv add fastapi python-jose[cryptography] httpx sqlalchemy +``` + +### JWT Validation Middleware + +```python +# src/auth/middleware.py +import logging +from typing import Dict, Optional +from fastapi import Depends, HTTPException, Request +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from jose import jwt, JWTError +import httpx + +logger = logging.getLogger(__name__) + +# Security scheme +security = HTTPBearer() + +class KeycloakJWTBearer: + """JWT Bearer token validator for Keycloak using python-jose""" + + def __init__(self): + # Keycloak configuration from environment + self.keycloak_url = os.getenv('KEYCLOAK_URL', 'http://localhost:8080') + self.realm = os.getenv('KEYCLOAK_REALM', 'ai-platform') + self.client_id = os.getenv('KEYCLOAK_CLIENT_ID', 'ai-platform-client') + + # Build OIDC discovery URL + self.oidc_discovery_url = f"{self.keycloak_url}/realms/{self.realm}/.well-known/openid-configuration" + + # Caching + self._oidc_config = None + self._jwks = None + self._cache_time = 0 + self._cache_ttl = 3600 # 1 hour + + async def get_oidc_config(self) -> dict: + """Fetch OIDC configuration from Keycloak (with caching)""" + current_time = time.time() + + if self._oidc_config and (current_time - self._cache_time) < self._cache_ttl: + return self._oidc_config + + try: + async with httpx.AsyncClient() as client: + response = await client.get(self.oidc_discovery_url, timeout=10) + response.raise_for_status() + self._oidc_config = response.json() + self._cache_time = current_time + logger.info(f"✅ Fetched OIDC config from {self.oidc_discovery_url}") + return self._oidc_config + except Exception as e: + logger.error(f"❌ Failed to fetch OIDC config: {e}") + raise HTTPException( + status_code=503, + detail='Authentication service unavailable' + ) from e + + async def get_jwks(self) -> dict: + """Fetch JWKS (JSON Web Key Set) from Keycloak""" + oidc_config = await self.get_oidc_config() + jwks_uri = oidc_config.get('jwks_uri') + + if not jwks_uri: + raise HTTPException(status_code=500, detail='JWKS URI not found') + + try: + async with httpx.AsyncClient() as client: + response = await client.get(jwks_uri, timeout=10) + response.raise_for_status() + self._jwks = response.json() + logger.info(f"✅ Fetched JWKS from {jwks_uri}") + return self._jwks + except Exception as e: + logger.error(f"❌ Failed to fetch JWKS: {e}") + raise HTTPException( + status_code=503, + detail='Authentication service unavailable' + ) from e + + async def validate_token(self, token: str) -> dict: + """ + Validate JWT token and return claims using python-jose + + Returns: + dict: Token claims including sub, email, preferred_username, realm_access + + Raises: + HTTPException: If token is invalid or expired + """ + logger.info(f'🔍 Validating JWT token (length: {len(token)})') + + try: + # Get OIDC config and JWKS + oidc_config = await self.get_oidc_config() + jwks = await self.get_jwks() + + # Decode and validate token + claims = jwt.decode( + token, + jwks, + algorithms=['RS256'], # Keycloak uses RS256 + issuer=oidc_config['issuer'], + options={ + 'verify_exp': True, # Verify expiration + 'verify_aud': False, # Manually verify audience (see below) + }, + ) + + # Manual audience verification (more flexible for public clients) + if 'aud' in claims: + audience = claims.get('aud') + valid_audiences = [self.client_id, 'account'] + audience_list = [audience] if isinstance(audience, str) else audience + + if not any(aud in valid_audiences for aud in audience_list): + logger.error(f'❌ Invalid audience: {audience}') + raise JWTError('Invalid audience') + + logger.info('✅ Token validation successful') + logger.info(f' Subject: {claims.get("sub")}') + logger.info(f' Email: {claims.get("email")}') + logger.info(f' Roles: {claims.get("realm_access", {}).get("roles", [])}') + + return claims + + except JWTError as e: + logger.error(f'❌ JWT validation error: {e}') + raise HTTPException(status_code=401, detail='Invalid token') from e + except Exception as e: + logger.error(f'❌ Token validation error: {e}') + raise HTTPException( + status_code=401, + detail='Token validation failed' + ) from e + +# Global instance +keycloak_jwt = KeycloakJWTBearer() +``` + +### Authentication Dependencies + +```python +# src/auth/middleware.py (continued) +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select +from db.models import User + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + session: AsyncSession = Depends(get_db), +) -> dict: + """ + Extract user info from JWT token (optional - returns None if no token) + + ⚠️ CRITICAL: User ID Mapping + This function handles the mapping between Keycloak user ID (sub claim) + and your application's database user ID. + """ + if not credentials: + return None + + # Validate token and get claims + claims = await keycloak_jwt.validate_token(credentials.credentials) + + # Get Keycloak user ID from 'sub' claim + keycloak_id = claims.get('sub') + user_email = claims.get('email') + + # ⚠️ USER ID MAPPING - IMPORTANT! + # By default, use Keycloak ID as user_id + user_id = keycloak_id + + # If your app has existing users, map Keycloak ID to app user ID + if session and keycloak_id: + try: + # Primary lookup: by keycloak_id (fast, recommended) + result = await session.execute( + select(User).where(User.keycloak_id == keycloak_id) + ) + db_user = result.scalar_one_or_none() + + if db_user: + user_id = db_user.id # Use app's internal user ID + logger.info(f'✅ Mapped Keycloak ID to app user: {user_id}') + elif user_email: + # Fallback: lookup by email and update keycloak_id + result = await session.execute( + select(User).where(User.email == user_email) + ) + db_user = result.scalar_one_or_none() + + if db_user: + # Link Keycloak ID to existing user + db_user.keycloak_id = keycloak_id + await session.commit() + user_id = db_user.id + logger.info(f'✅ Linked Keycloak ID to existing user: {user_id}') + + except Exception as e: + logger.warning(f'⚠️ Database lookup failed: {e}') + # Fall back to Keycloak ID + + return { + 'id': user_id, # App user ID (mapped from Keycloak) + 'email': claims.get('email'), + 'username': claims.get('preferred_username'), + 'name': claims.get('name'), + 'roles': claims.get('realm_access', {}).get('roles', []), + 'keycloak_id': keycloak_id, # Original Keycloak ID + 'token_claims': claims, # Full token claims + } + + +async def require_authentication( + credentials: HTTPAuthorizationCredentials = Depends(security), + session: AsyncSession = Depends(get_db), +) -> dict: + """Require valid JWT token (raises 401 if missing or invalid)""" + if not credentials: + raise HTTPException( + status_code=401, + detail='Authentication required', + headers={'WWW-Authenticate': 'Bearer'}, + ) + + return await get_current_user(credentials, session) + + +def require_role(required_role: str): + """Dependency factory to require specific role""" + async def check_role(current_user: dict = Depends(require_authentication)) -> dict: + user_roles = current_user.get('roles', []) + if required_role not in user_roles: + raise HTTPException( + status_code=403, + detail=f'Required role: {required_role}' + ) + return current_user + return check_role + + +# Convenience dependencies +require_admin = require_role('admin') +require_user = require_role('user') +``` + +### Protecting API Endpoints + +```python +# src/routes/users.py +from fastapi import APIRouter, Depends +from sqlalchemy.ext.asyncio import AsyncSession +from auth.middleware import ( + get_current_user, + require_authentication, + require_admin, +) + +router = APIRouter(prefix='/users', tags=['users']) + +@router.get('/profile') +async def get_current_user_profile( + session: AsyncSession = Depends(get_db), + current_user: dict = Depends(require_authentication), # ✅ Protected +): + """ + Get the current logged-in user profile + + This endpoint demonstrates: + 1. How to protect an endpoint with authentication + 2. How to access the current user's information + 3. How to query user-specific data from the database + """ + # current_user contains validated token claims + user_id = current_user['id'] # App's internal user ID + + # Query database for user details + result = await session.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException(status_code=404, detail='User not found') + + return { + 'id': user.id, + 'email': user.email, + 'first_name': user.first_name, + 'last_name': user.last_name, + 'keycloak_id': user.keycloak_id, # For reference + # ... other user fields + } + + +@router.get('/') +async def list_users( + session: AsyncSession = Depends(get_db), + current_user: dict = Depends(require_admin), # ✅ Admin only +): + """List all users (admin only)""" + result = await session.execute(select(User)) + users = result.scalars().all() + return users + + +@router.get('/{user_id}') +async def get_user( + user_id: str, + session: AsyncSession = Depends(get_db), + current_user: dict = Depends(require_authentication), +): + """Get specific user (with authorization check)""" + result = await session.execute( + select(User).where(User.id == user_id) + ) + user = result.scalar_one_or_none() + + if not user: + raise HTTPException(status_code=404, detail='User not found') + + # Authorization: Users can only access their own data, admins can access any + if ('admin' not in current_user.get('roles', []) and + current_user['id'] != user_id): + raise HTTPException(status_code=403, detail='Access denied') + + return user +``` + +--- + +## Configuration + +### Required Environment Variables + +#### Frontend (.env) + +```bash +# Keycloak Configuration +VITE_KEYCLOAK_URL=https://keycloak.example.com/realms/ai-platform +VITE_KEYCLOAK_CLIENT_ID=ai-platform-client +VITE_KEYCLOAK_REDIRECT_URI=https://app.example.com +VITE_KEYCLOAK_POST_LOGOUT_REDIRECT_URI=https://app.example.com + +# API Configuration +VITE_API_BASE_URL=https://api.example.com + +# Optional: Development bypass +VITE_BYPASS_AUTH=false +VITE_ENVIRONMENT=production +``` + +#### Backend (.env) + +```bash +# Keycloak Configuration +KEYCLOAK_URL=https://keycloak.example.com +KEYCLOAK_REALM=ai-platform +KEYCLOAK_CLIENT_ID=ai-platform-client + +# Database +DATABASE_URL=postgresql+asyncpg://user:password@localhost:5432/myapp + +# Optional: Development bypass +BYPASS_AUTH=false +ENVIRONMENT=production +``` + +### Keycloak OIDC Auto-Discovery URL + +The OIDC discovery document is automatically available at: + +``` +{KEYCLOAK_URL}/realms/{REALM}/.well-known/openid-configuration +``` + +**Example:** +``` +https://keycloak.example.com/realms/ai-platform/.well-known/openid-configuration +``` + +This URL provides: +- `issuer`: Token issuer +- `authorization_endpoint`: OAuth authorization URL +- `token_endpoint`: Token exchange URL +- `jwks_uri`: Public keys for JWT validation +- `userinfo_endpoint`: User info endpoint +- `end_session_endpoint`: Logout URL + +### Keycloak Client Configuration + +In Keycloak Admin Console, configure your client: + +1. **Client ID**: `ai-platform-client` +2. **Client Type**: `OpenID Connect` +3. **Client authentication**: `Off` (public client for frontend) +4. **Authorization**: `Off` +5. **Standard flow**: `Enabled` (Authorization Code Flow) +6. **Direct access grants**: `Enabled` (for testing) +7. **Valid redirect URIs**: + - `https://app.example.com/*` + - `http://localhost:3000/*` (for development) +8. **Valid post logout redirect URIs**: Same as redirect URIs +9. **Web origins**: + - `https://app.example.com` + - `http://localhost:3000` (for development) + +--- + +## Protected Resources + +### Frontend Route Protection + +```typescript +// src/router.tsx +import { createRoute } from '@tanstack/react-router'; +import { ProtectedRoute } from './components/ProtectedRoute'; + +export const dashboardRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/dashboard', + component: () => ( + + + + ), +}); + +// Public routes (no protection) +export const loginRoute = createRoute({ + getParentRoute: () => rootRoute, + path: '/login', + component: LoginPage, +}); +``` + +### Backend Endpoint Protection Patterns + +```python +# Pattern 1: Optional authentication (returns None if not authenticated) +@router.get('/public-data') +async def get_public_data( + current_user: dict = Depends(get_current_user), # Optional +): + """Endpoint accessible to both authenticated and anonymous users""" + if current_user: + # Personalized data + return {'message': f'Hello {current_user["email"]}'} + else: + # Generic data + return {'message': 'Hello anonymous user'} + + +# Pattern 2: Required authentication +@router.get('/protected-data') +async def get_protected_data( + current_user: dict = Depends(require_authentication), # Required +): + """Endpoint requires valid JWT token""" + return {'data': 'secret', 'user': current_user['email']} + + +# Pattern 3: Role-based access +@router.get('/admin-only') +async def admin_endpoint( + current_user: dict = Depends(require_admin), # Admin role required +): + """Endpoint requires admin role""" + return {'admin_data': 'sensitive information'} + + +# Pattern 4: Custom authorization logic +@router.get('/resource/{resource_id}') +async def get_resource( + resource_id: str, + current_user: dict = Depends(require_authentication), + session: AsyncSession = Depends(get_db), +): + """Endpoint with custom authorization logic""" + resource = await get_resource_by_id(session, resource_id) + + # Check if user owns the resource + if resource.owner_id != current_user['id'] and 'admin' not in current_user['roles']: + raise HTTPException(status_code=403, detail='Not authorized') + + return resource +``` + +--- + +## User Profile Management + +### Frontend: Fetching User Profile + +```typescript +// src/hooks/useCurrentUser.ts +import { useQuery } from '@tanstack/react-query'; +import { ApiClient } from '../services/apiClient'; + +export interface UserProfile { + id: string; + email: string; + first_name: string; + last_name: string; + keycloak_id: string; + is_active: boolean; + created_at: string; + // ... other fields +} + +export function useCurrentUser() { + return useQuery({ + queryKey: ['currentUser'], + queryFn: () => ApiClient.get('/users/profile'), + enabled: true, // Only fetch if authenticated + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + }); +} +``` + +```typescript +// src/pages/Profile.tsx +import { useCurrentUser } from '../hooks/useCurrentUser'; + +export function ProfilePage() { + const { data: user, isLoading, error } = useCurrentUser(); + + if (isLoading) return
Loading profile...
; + if (error) return
Error loading profile: {error.message}
; + + return ( +
+

My Profile

+

Email: {user?.email}

+

Name: {user?.first_name} {user?.last_name}

+

Keycloak ID: {user?.keycloak_id}

+
+ ); +} +``` + +### Backend: User Profile Endpoint + +Already shown above in the "Protecting API Endpoints" section. + +--- + +## User ID Mapping (CRITICAL!) + +### The Problem + +When integrating Keycloak with an existing application that has its own users table, you face a **user ID mismatch** problem: + +- **Keycloak** assigns its own user IDs (UUID format, e.g., `a1b2c3d4-...`) +- **Your app** may have existing user IDs (integer, UUID, or other format) +- **Foreign keys** in your database reference your app's user IDs, not Keycloak's + +### The Solution: keycloak_id Column + +Add a `keycloak_id` column to your users table to map between the two ID systems. + +#### Database Migration + +```python +# alembic/versions/xxx_add_keycloak_id.py +def upgrade(): + op.add_column('users', sa.Column('keycloak_id', sa.String(), nullable=True)) + op.create_unique_constraint('uq_users_keycloak_id', 'users', ['keycloak_id']) + op.create_index('ix_users_keycloak_id', 'users', ['keycloak_id']) + +def downgrade(): + op.drop_index('ix_users_keycloak_id') + op.drop_constraint('uq_users_keycloak_id', 'users') + op.drop_column('users', 'keycloak_id') +``` + +#### Database Model + +```python +# db/models.py +from sqlalchemy.orm import Mapped, mapped_column +from sqlalchemy import String + +class User(Base): + __tablename__ = 'users' + + id: Mapped[int] = mapped_column(primary_key=True) # App's internal ID + email: Mapped[str] = mapped_column(String, unique=True, nullable=False) + keycloak_id: Mapped[str | None] = mapped_column( + String, + unique=True, + nullable=True, + index=True, # Index for fast lookups + ) + first_name: Mapped[str] + last_name: Mapped[str] + # ... other fields +``` + +#### User Lookup Strategy + +The middleware (shown earlier) implements a **three-tier lookup strategy**: + +1. **Primary**: Lookup by `keycloak_id` (fast, indexed) +2. **Fallback**: If not found, lookup by email +3. **Auto-link**: If found by email, update `keycloak_id` for future fast lookups +4. **Default**: If neither found, use Keycloak ID as user_id + +This strategy ensures: +- ✅ Existing users are automatically linked to Keycloak +- ✅ New users work immediately +- ✅ Performance improves over time as mappings are created +- ✅ No manual user migration required + +### Migration Workflow + +```mermaid +graph TD + A[User logs in with Keycloak] --> B{keycloak_id exists?} + B -->|Yes| C[Fast lookup by keycloak_id] + C --> D[Use app user ID] + B -->|No| E[Lookup by email] + E --> F{User found?} + F -->|Yes| G[Update keycloak_id] + G --> H[Link accounts] + H --> D + F -->|No| I[Create new user
or use Keycloak ID] + I --> D + D --> J[Return user context] +``` + +--- + +## Role-Based Access Control + +### Keycloak Realm Roles + +In Keycloak Admin Console: + +1. Navigate to: Realm → Roles +2. Create roles: + - `admin` - Full access + - `user` - Standard user access + - `viewer` - Read-only access + - Custom roles as needed + +### Assign Roles to Users + +1. Navigate to: Users → Select User → Role mapping +2. Assign realm roles + +### Extract Roles from Token + +Roles are included in the JWT token under `realm_access.roles`: + +```json +{ + "sub": "a1b2c3d4-...", + "email": "user@example.com", + "realm_access": { + "roles": ["user", "admin"] + } +} +``` + +### Frontend: Role-Based UI + +```typescript +// src/hooks/useAuth.ts +export function useAuth() { + const { user } = useContext(AuthContext); + + const hasRole = (role: string) => { + return user?.roles.includes(role) || false; + }; + + const isAdmin = () => hasRole('admin'); + + return { + user, + hasRole, + isAdmin, + // ... other auth functions + }; +} +``` + +```typescript +// src/components/AdminPanel.tsx +import { useAuth } from '../hooks/useAuth'; + +export function AdminPanel() { + const { isAdmin } = useAuth(); + + if (!isAdmin()) { + return
Access denied. Admin privileges required.
; + } + + return
Admin content here
; +} +``` + +### Backend: Role-Based Endpoints + +Already shown in the "Authentication Dependencies" section with `require_role` factory. + +### Client Roles (Advanced) + +For more granular permissions, use **client roles**: + +1. In Keycloak: Clients → {your-client} → Roles +2. Create client-specific roles +3. Assign to users +4. Extract from token under `resource_access.{client_id}.roles` + +```python +# Extract client roles +def get_client_roles(claims: dict, client_id: str) -> list[str]: + return ( + claims.get('resource_access', {}) + .get(client_id, {}) + .get('roles', []) + ) +``` + +--- + +## User Registration + +### ⚠️ Not Implemented in Spending Transaction Monitor + +User registration was not implemented in the reference project, but here's how to add it: + +### Option 1: Keycloak Self-Registration (Recommended) + +Enable self-registration in Keycloak: + +1. **Keycloak Admin Console**: + - Realm Settings → Login → User registration: `On` + - Configure email verification + - Set up email server (SMTP) + +2. **Registration Flow**: + ``` + User → Click "Register" → Keycloak registration form → + Email verification → Account created → Auto-login + ``` + +3. **Sync to App Database**: + - Create a webhook or scheduled job to sync new Keycloak users to your app database + - Or create users on first login (lazy creation) + +### Option 2: Custom Registration API + +Implement registration in your backend: + +```python +# src/routes/auth.py +from keycloak import KeycloakAdmin + +keycloak_admin = KeycloakAdmin( + server_url=settings.KEYCLOAK_URL, + realm_name=settings.KEYCLOAK_REALM, + client_id=settings.KEYCLOAK_CLIENT_ID, + client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, + verify=True, +) + +@router.post('/register') +async def register_user( + email: str, + password: str, + first_name: str, + last_name: str, + session: AsyncSession = Depends(get_db), +): + """Register a new user in both Keycloak and app database""" + try: + # 1. Create user in Keycloak + keycloak_user_id = keycloak_admin.create_user({ + 'email': email, + 'username': email, + 'enabled': True, + 'firstName': first_name, + 'lastName': last_name, + 'credentials': [{ + 'type': 'password', + 'value': password, + 'temporary': False, + }], + 'emailVerified': False, # Require email verification + }) + + # 2. Send verification email + keycloak_admin.send_verify_email(keycloak_user_id) + + # 3. Create user in app database + new_user = User( + email=email, + keycloak_id=keycloak_user_id, + first_name=first_name, + last_name=last_name, + ) + session.add(new_user) + await session.commit() + + return { + 'message': 'User created. Please check your email for verification.', + 'user_id': new_user.id, + } + + except Exception as e: + logger.error(f'Registration failed: {e}') + raise HTTPException(status_code=400, detail=str(e)) +``` + +### Option 3: Lazy User Creation + +Create users in your app database on first login: + +```python +async def get_current_user(...): + claims = await keycloak_jwt.validate_token(...) + keycloak_id = claims.get('sub') + + # Try to find existing user + result = await session.execute( + select(User).where(User.keycloak_id == keycloak_id) + ) + db_user = result.scalar_one_or_none() + + # If user doesn't exist, create them + if not db_user: + db_user = User( + keycloak_id=keycloak_id, + email=claims.get('email'), + first_name=claims.get('given_name', ''), + last_name=claims.get('family_name', ''), + ) + session.add(db_user) + await session.commit() + logger.info(f'✨ Created new user from Keycloak: {db_user.email}') + + return { + 'id': db_user.id, + # ... other fields + } +``` + +--- + +## Best Practices + +### Security + +1. **✅ Use HTTPS in Production** + - Always use TLS for Keycloak and your application + - Never send tokens over unencrypted connections + +2. **✅ Validate Tokens Properly** + - Verify signature using JWKS + - Check issuer, audience, and expiration + - Use established libraries (python-jose, react-oidc-context) + +3. **✅ Store Tokens Securely** + - Frontend: Memory or localStorage (not cookies for public clients) + - Backend: Never store access tokens (validate on each request) + - Use refresh tokens for long sessions + +4. **✅ Implement Token Refresh** + - Refresh tokens before they expire + - Handle token refresh failures gracefully + +5. **✅ Use Short Token Lifespans** + - Access tokens: 5-15 minutes + - Refresh tokens: Hours to days + - Adjust based on security requirements + +6. **✅ Validate Audience** + - Ensure tokens are intended for your client + +7. **✅ Protect Against CSRF** + - Use state parameter in OAuth flow + - Implement PKCE for public clients + +### Performance + +1. **✅ Cache OIDC Configuration** + - Cache discovery document and JWKS + - Refresh periodically (e.g., 1 hour) + +2. **✅ Index keycloak_id Column** + - Fast user lookups + - Essential for performance at scale + +3. **✅ Avoid User Info Endpoint** + - Use claims from ID token instead + - Reduces network requests + +4. **✅ Implement Connection Pooling** + - Reuse HTTP connections to Keycloak + - Use async HTTP clients (httpx, aiohttp) + +### User Experience + +1. **✅ Handle Auth Errors Gracefully** + - Show user-friendly error messages + - Provide clear login instructions + +2. **✅ Implement Silent Authentication** + - Automatic token refresh + - Seamless re-authentication when possible + +3. **✅ Remember User Session** + - Persist auth state across page reloads + - Use localStorage or sessionStorage + +4. **✅ Provide Logout Functionality** + - Clear local state + - Redirect to Keycloak logout + - Invalidate backend sessions + +### Development + +1. **✅ Use Auth Bypass for Development** + - Toggle authentication with environment variable + - Speeds up local development + - Never enable in production + +2. **✅ Test with Multiple Users** + - Create test users with different roles + - Test authorization logic thoroughly + +3. **✅ Log Auth Events** + - Log successful/failed authentications + - Track token validation + - Monitor for suspicious activity + +4. **✅ Document Required Secrets** + - Clearly document all environment variables + - Provide example configurations + +--- + +## Troubleshooting + +### Common Issues + +#### 1. "Invalid issuer" Error + +**Problem**: Token issuer doesn't match expected issuer. + +**Solution**: +- Check `KEYCLOAK_URL` matches exactly (including protocol and realm) +- Verify Keycloak is accessible from backend +- Check for trailing slashes in URLs + +```bash +# Test OIDC discovery +curl https://keycloak.example.com/realms/ai-platform/.well-known/openid-configuration +``` + +#### 2. "CORS Error" in Frontend + +**Problem**: Keycloak blocks requests from your frontend origin. + +**Solution**: +- Add your frontend URL to "Web Origins" in Keycloak client settings +- Use wildcards for development: `http://localhost:*` + +#### 3. Token Validation Fails + +**Problem**: Backend rejects valid tokens. + +**Solution**: +- Check clock synchronization (token exp claim) +- Verify JWKS is fetched correctly +- Check audience claim matches client ID +- Enable debug logging + +```python +# Enable debug logging +logging.basicConfig(level=logging.DEBUG) +``` + +#### 4. User Not Found After Login + +**Problem**: User exists in Keycloak but not in app database. + +**Solution**: +- Implement lazy user creation (shown above) +- Or sync users from Keycloak to database +- Check `keycloak_id` mapping logic + +#### 5. Infinite Redirect Loop + +**Problem**: Frontend keeps redirecting to login. + +**Solution**: +- Check redirect URIs in Keycloak client settings +- Verify token is being stored correctly +- Check browser localStorage/cookies +- Ensure `isAuthenticated` state updates properly + +#### 6. "403 Forbidden" on Protected Endpoints + +**Problem**: User is authenticated but doesn't have required role. + +**Solution**: +- Verify roles are assigned in Keycloak +- Check role extraction from token +- Confirm role names match exactly (case-sensitive) + +### Debug Checklist + +```bash +# 1. Test Keycloak connectivity +curl https://keycloak.example.com/realms/ai-platform/.well-known/openid-configuration + +# 2. Verify JWKS endpoint +curl https://keycloak.example.com/realms/ai-platform/protocol/openid-connect/certs + +# 3. Test token endpoint (get access token) +curl -X POST https://keycloak.example.com/realms/ai-platform/protocol/openid-connect/token \ + -d "client_id=ai-platform-client" \ + -d "username=testuser" \ + -d "password=testpass" \ + -d "grant_type=password" + +# 4. Decode JWT token (paste token at jwt.io) +echo $TOKEN | jwt decode - + +# 5. Test API endpoint with token +curl -H "Authorization: Bearer $TOKEN" https://api.example.com/users/profile +``` + +--- + +## Summary + +### Required Steps for Integration + +1. **Keycloak Setup**: + - ✅ Create realm + - ✅ Create client (public, OIDC) + - ✅ Configure redirect URIs + - ✅ Create realm roles + - ✅ Create test users + +2. **Database**: + - ✅ Add `keycloak_id` column to users table + - ✅ Create unique index on `keycloak_id` + - ✅ Implement user lookup logic + +3. **Frontend**: + - ✅ Install `react-oidc-context` + - ✅ Configure OIDC provider + - ✅ Implement auth context + - ✅ Add Bearer token to API requests + - ✅ Protect routes + +4. **Backend**: + - ✅ Install `python-jose` + - ✅ Implement JWT validation + - ✅ Create authentication dependencies + - ✅ Protect endpoints + - ✅ Implement role-based access control + +5. **Configuration**: + - ✅ Set environment variables + - ✅ Configure CORS + - ✅ Test auto-discovery URL + +6. **Testing**: + - ✅ Test authentication flow + - ✅ Test protected endpoints + - ✅ Test role-based access + - ✅ Test user ID mapping + +--- + +## Additional Resources + +- [Keycloak Documentation](https://www.keycloak.org/documentation) +- [OAuth 2.0 Specification](https://oauth.net/2/) +- [OpenID Connect Specification](https://openid.net/specs/openid-connect-core-1_0.html) +- [react-oidc-context](https://github.com/authts/react-oidc-context) +- [python-jose](https://github.com/mpdavis/python-jose) +- [FastAPI Security](https://fastapi.tiangolo.com/tutorial/security/) +- [JWT.io](https://jwt.io/) - Debug JWT tokens + +--- + +## Support + +For questions or issues: +- Reference Implementation: [Spending Transaction Monitor](https://github.com/example/spending-transaction-monitor) +- Keycloak Helm Chart: See main README.md +- Community Support: Create an issue in the repository + +--- + +**Last Updated**: 2025-01-03 +**Tested With**: +- Keycloak 26.0.7 +- react-oidc-context 3.3.0 +- python-jose 3.3.0 +- FastAPI 0.104.0 + diff --git a/keycloak/helm/README.md b/keycloak/helm/README.md index e5b6e11..231bd98 100644 --- a/keycloak/helm/README.md +++ b/keycloak/helm/README.md @@ -105,6 +105,135 @@ echo "Username: $ADMIN_USER" echo "Password: $ADMIN_PASS" ``` +## Application Integration Quick Start + +Once Keycloak is deployed, integrate authentication in your application in 5 minutes. + +### Frontend (React) + +```bash +npm install react-oidc-context +``` + +```typescript +// src/main.tsx +import { AuthProvider } from 'react-oidc-context'; + +const oidcConfig = { + authority: 'https://keycloak.example.com/realms/ai-platform', + client_id: 'ai-platform-client', + redirect_uri: window.location.origin, + response_type: 'code', + scope: 'openid profile email', +}; + +root.render( + + + +); +``` + +```typescript +// src/App.tsx +import { useAuth } from 'react-oidc-context'; + +function App() { + const auth = useAuth(); + + if (auth.isLoading) return
Loading...
; + if (!auth.isAuthenticated) { + return ; + } + + return ( +
+

Welcome {auth.user?.profile.email}

+ +
+ ); +} +``` + +### Backend (Python/FastAPI) + +```bash +pip install fastapi python-jose[cryptography] httpx +``` + +```python +# src/auth/middleware.py +from fastapi import Depends, HTTPException +from fastapi.security import HTTPBearer +from jose import jwt +import httpx + +security = HTTPBearer() + +async def validate_token(token: str) -> dict: + """Validate JWT token using Keycloak JWKS""" + async with httpx.AsyncClient() as client: + config_url = "https://keycloak.example.com/realms/ai-platform/.well-known/openid-configuration" + config = (await client.get(config_url)).json() + jwks = (await client.get(config['jwks_uri'])).json() + + return jwt.decode(token, jwks, algorithms=['RS256'], issuer=config['issuer']) + +async def get_current_user(credentials = Depends(security)): + """Extract user from JWT""" + if not credentials: + raise HTTPException(status_code=401, detail='Not authenticated') + + claims = await validate_token(credentials.credentials) + return { + 'id': claims['sub'], + 'email': claims['email'], + 'roles': claims.get('realm_access', {}).get('roles', []), + } + +# src/main.py +from fastapi import FastAPI, Depends + +app = FastAPI() + +@app.get('/profile') +async def get_profile(current_user = Depends(get_current_user)): + """Protected endpoint""" + return {'user': current_user} +``` + +### Environment Variables + +```bash +# Frontend (.env) +VITE_KEYCLOAK_URL=https://keycloak.example.com/realms/ai-platform +VITE_KEYCLOAK_CLIENT_ID=ai-platform-client + +# Backend (.env) +KEYCLOAK_URL=https://keycloak.example.com +KEYCLOAK_REALM=ai-platform +KEYCLOAK_CLIENT_ID=ai-platform-client +``` + +### Key URLs + +| URL | Purpose | +|-----|---------| +| `{KEYCLOAK_URL}/realms/{realm}/.well-known/openid-configuration` | OIDC auto-discovery | +| `{KEYCLOAK_URL}/realms/{realm}/protocol/openid-connect/certs` | JWKS (public keys) | +| `{KEYCLOAK_URL}/admin` | Admin console | + +### Common Issues + +- **"Invalid issuer"**: Check `KEYCLOAK_URL` matches exactly +- **"CORS error"**: Add your frontend URL to Keycloak client "Web Origins" +- **"Token validation fails"**: Verify JWKS endpoint, check system time +- **"User not found"**: Implement user ID mapping (see integration guide) + +For complete integration details including user ID mapping, role-based access, and production best practices, see [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md). + +--- + ## Configuration ### Basic Configuration @@ -441,12 +570,37 @@ To contribute: 3. Test thoroughly 4. Submit a pull request +## Complete Integration Guide + +For comprehensive integration documentation, see **[INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md)** covering: + +- **Architecture & Flow**: Detailed sequence diagrams and OAuth 2.0 / OIDC flow +- **Frontend Integration**: Complete React with `react-oidc-context` implementation +- **Backend Integration**: FastAPI with `python-jose` JWT validation +- **User ID Mapping**: Critical for apps with existing users (database schema, migration) +- **Protected Resources**: Frontend routes and backend endpoint patterns +- **Role-Based Access Control**: Realm and client roles +- **User Registration**: Three strategies for new user onboarding +- **Best Practices**: Security, performance, and production readiness +- **Troubleshooting**: Common issues and debug techniques + +### Reference Implementation + +The [Spending Transaction Monitor](https://github.com/rh-ai-quickstart/spending-transaction-monitor) demonstrates: +- Complete React + FastAPI authentication +- User ID mapping between Keycloak and app database +- Protected routes and endpoints +- Role-based authorization +- Profile management (`/users/profile` endpoint) + ## Resources +- [INTEGRATION_GUIDE.md](INTEGRATION_GUIDE.md) - Complete application integration guide - [Keycloak Documentation](https://www.keycloak.org/documentation) - [Keycloak Operator Guide](https://www.keycloak.org/guides#operator) - [Keycloak on OpenShift](https://www.keycloak.org/getting-started/getting-started-openshift) - [AI Architecture Charts](https://github.com/rh-ai-quickstart/ai-architecture-charts) +- [Spending Transaction Monitor](https://github.com/rh-ai-quickstart/spending-transaction-monitor) - Reference implementation ## License