Files
work-club-manager/.sisyphus/scripts/KEYCLOAK_UPDATE_GUIDE.md
WorkClub Automation e8c8dac5d4 fix(keycloak): update user club attributes with real database UUIDs
- Replaced placeholder UUIDs (club-1-uuid, club-2-uuid) with real database UUIDs
- Updated all 5 test users via Keycloak database
- Restarted Keycloak to clear caches and apply changes

Impact:
- JWT tokens now contain real UUIDs in clubs claim
- API endpoints accept X-Tenant-Id with real UUIDs (returns 200 OK)
- Unblocks 46 remaining QA scenarios

Documentation:
- Created update-keycloak-club-uuids.py script for automation
- Added KEYCLOAK_UPDATE_GUIDE.md with step-by-step instructions
- Recorded learnings in notepad

Ref: .sisyphus/evidence/final-f3-manual-qa.md lines 465-512
2026-03-05 14:21:44 +01:00

6.9 KiB

Keycloak Club UUID Update Guide

Overview

This guide documents the process of updating Keycloak user attributes to replace placeholder club UUIDs with real database UUIDs. This ensures JWT tokens contain the correct tenant identifiers for API access.

Blocker Resolution

Blocker #2 (Critical): JWT clubs claim uses placeholder strings instead of real database UUIDs

Before

{
  "clubs": {
    "club-1-uuid": "admin",
    "club-2-uuid": "member"
  }
}

After

{
  "clubs": {
    "afa8daf3-5cfa-4589-9200-b39a538a12de": "admin",
    "a1952a72-2e13-4a4e-87dd-821847b58698": "member"
  }
}

Real Club UUIDs

From PostgreSQL clubs table:

UUID Name Sport Type
afa8daf3-5cfa-4589-9200-b39a538a12de Sunrise Tennis Club Tennis (0)
a1952a72-2e13-4a4e-87dd-821847b58698 Valley Cycling Club Cycling (1)

Test Users Configuration

After update, the 5 test users have these club assignments:

admin@test.com       → Admin in Sunrise Tennis + Member in Valley Cycling
manager@test.com     → Manager in Sunrise Tennis + Member in Valley Cycling
member1@test.com     → Member in Sunrise Tennis + Member in Valley Cycling
member2@test.com     → Member in Valley Cycling
viewer@test.com      → Viewer in Sunrise Tennis

Update Methods

Run the complete update script:

python3 .sisyphus/scripts/update-keycloak-club-uuids.py

What it does:

  1. ✓ Updates clubs attributes in PostgreSQL database
  2. ✓ Restarts Keycloak to clear caches
  3. ✓ Verifies JWT tokens contain real UUIDs
  4. ✓ Tests API endpoints with real UUIDs

Method 2: Manual Database Update

If you only need to update the database (without Keycloak restart):

docker exec workclub_postgres psql -U postgres -d keycloak << 'SQL'
UPDATE user_attribute SET value = '{"afa8daf3-5cfa-4589-9200-b39a538a12de": "admin", "a1952a72-2e13-4a4e-87dd-821847b58698": "member"}' WHERE user_id = 'bf5adcfb-0978-4beb-8e02-7577f0ded47f' AND name = 'clubs';

UPDATE user_attribute SET value = '{"afa8daf3-5cfa-4589-9200-b39a538a12de": "manager", "a1952a72-2e13-4a4e-87dd-821847b58698": "member"}' WHERE user_id = 'aa5270a3-633a-4d89-a3b4-a467b08cbb55' AND name = 'clubs';

UPDATE user_attribute SET value = '{"afa8daf3-5cfa-4589-9200-b39a538a12de": "member", "a1952a72-2e13-4a4e-87dd-821847b58698": "member"}' WHERE user_id = '60c0d8b9-6354-4ad3-bfac-9547c68c069b' AND name = 'clubs';

UPDATE user_attribute SET value = '{"a1952a72-2e13-4a4e-87dd-821847b58698": "member"}' WHERE user_id = '294a2086-cf2f-43cc-9bc6-2a8a7d325b9a' AND name = 'clubs';

UPDATE user_attribute SET value = '{"afa8daf3-5cfa-4589-9200-b39a538a12de": "viewer"}' WHERE user_id = 'f4890d47-ba6c-4691-9d7b-4f656c60f232' AND name = 'clubs';
SQL

Then restart Keycloak:

docker restart workclub_keycloak && sleep 10

Method 3: Keycloak Admin API

⚠️ Note: The Keycloak Admin API (PUT /admin/realms/workclub/users/{id}) returns 204 No Content but doesn't persist attribute changes in this setup. Database updates are required.

Verification

1. Check Database

docker exec workclub_postgres psql -U postgres -d keycloak -c "SELECT user_id, value FROM user_attribute WHERE name = 'clubs';"

Expected output shows real UUIDs:

afa8daf3-5cfa-4589-9200-b39a538a12de": "admin"
a1952a72-2e13-4a4e-87dd-821847b58698": "member"

2. Check JWT Token

# Get token
TOKEN=$(curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
  -d "client_id=workclub-app" \
  -d "grant_type=password" \
  -d "username=admin@test.com" \
  -d "password=testpass123" | jq -r '.access_token')

# Decode and check clubs claim
PAYLOAD=$(echo $TOKEN | cut -d'.' -f2)
PADDING=$((4 - ${#PAYLOAD} % 4))
if [ $PADDING -ne 4 ]; then
  PAYLOAD="${PAYLOAD}$(printf '%*s' $PADDING | tr ' ' '=')"
fi
echo "$PAYLOAD" | base64 -d 2>/dev/null | jq '.clubs'

Expected output:

{
  "afa8daf3-5cfa-4589-9200-b39a538a12de": "admin",
  "a1952a72-2e13-4a4e-87dd-821847b58698": "member"
}

3. Test API Endpoint

TOKEN=$(curl -s -X POST http://localhost:8080/realms/workclub/protocol/openid-connect/token \
  -d "client_id=workclub-app" \
  -d "grant_type=password" \
  -d "username=admin@test.com" \
  -d "password=testpass123" | jq -r '.access_token')

curl -X GET \
  -H "Authorization: Bearer $TOKEN" \
  -H "X-Tenant-Id: afa8daf3-5cfa-4589-9200-b39a538a12de" \
  http://localhost:5001/api/clubs/me

Expected: 200 OK response (should return [] or list of clubs)

Technical Details

How it Works

  1. User Attributes Storage: Keycloak stores user attributes in PostgreSQL user_attribute table
  2. Mapper Configuration: The club-membership mapper (oidc-usermodel-attribute-mapper) reads the clubs attribute and includes it in the JWT token
  3. Token Claim: The JWT clubs claim is generated from the clubs user attribute
  4. Caching: Keycloak caches user data in memory, so a restart is needed after database updates

Mapper Details

Client: workclub-app Mapper: club-membership Type: oidc-usermodel-attribute-mapper

Configuration:

- user.attribute: clubs
- claim.name: clubs
- jsonType.label: JSON
- id.token.claim: true
- access.token.claim: true
- introspection.token.claim: true
- userinfo.token.claim: true

User IDs in Database

admin@test.com       → bf5adcfb-0978-4beb-8e02-7577f0ded47f
manager@test.com     → aa5270a3-633a-4d89-a3b4-a467b08cbb55
member1@test.com     → 60c0d8b9-6354-4ad3-bfac-9547c68c069b
member2@test.com     → 294a2086-cf2f-43cc-9bc6-2a8a7d325b9a
viewer@test.com      → f4890d47-ba6c-4691-9d7b-4f656c60f232

Troubleshooting

JWT still shows old UUIDs

Problem: Database is updated but JWT token still has placeholder UUIDs

Solution:

  1. Restart Keycloak: docker restart workclub_keycloak
  2. Wait 10 seconds for Keycloak to boot
  3. Generate new token

API still rejects real UUID

Problem: API returns 401 or 403 with real UUID in X-Tenant-Id

Solution:

  1. Ensure Keycloak has restarted and token is fresh
  2. Check JWT token contains correct real UUIDs: jq '.clubs' <<< decoded_payload
  3. Verify database has correct values

Cannot connect to database

Problem: docker exec workclub_postgres fails

Solution:

  1. Check container is running: docker ps | grep postgres
  2. If container doesn't exist, start the workclub environment: docker-compose up -d
  3. Verify database: docker exec workclub_postgres psql -U postgres -d keycloak -c "SELECT 1"

Impact

This update resolves Blocker #2 (Critical) and unblocks:

  • All 46 remaining QA scenarios
  • Tenant resolution logic
  • Multi-club user workflows
  • API integration tests