Skip to content

Keycloak authentication

Keycloak is the OIDC identity provider for Sencai. It manages user registration, login, JWT tokens, TOTP/2FA, and SSO integrations.

  • Version: Latest
  • Realm: sencai
  • Port: 8080 (internal container port only, no dedicated external host port — reached exclusively through Traefik PathPrefix('/kc') on the shared web entrypoint)
  • Database: MySQL (keycloak-db:3307)
  • URL (internal): http://keycloak:8080/kc
  • Admin (local, via Traefik): http://app.sencai.localhost/kc/admin (creds: admin/admin)

Keycloak has multiple clients configured:

Client IDTypePurposeGrant Type
sencai-frontendPublicNuxt/Vue frontendauthorization_code
sencai-backendConfidentialStrapi backendclient_credentials
sencai-adminConfidentialAdmin UIauthorization_code
admin-cliConfidentialAdmin API (master realm)password
  • Access Token: 12 hours (KC_ACCESS_TOKEN_LIFESPAN=12h) at the realm level. The Nuxt frontend itself issues shorter-lived access_token/id_token cookies (15 min) and a 30-day refresh_token cookie (httpOnly) — see below.
  • Refresh Token: 30 days (httpOnly cookie)

The platform frontend (sencai.space-frontend) proactively refreshes the access token instead of waiting for it to expire:

  • A Nuxt plugin, auth-refresh-scheduler.client.ts, decodes the current access token’s exp claim and schedules a refresh 60 seconds before expiry. It re-arms on every token change (login, refresh, logout) and pauses while the browser tab is hidden.
  • The refresh itself is a single-flight action in the auth store: concurrent callers share one in-flight refresh, transient failures are retried, and only a genuine 401 from the refresh endpoint triggers logout (redirect to /auth/login with sessionExpired=1).
  • Server-side, POST /api/auth/refresh (a Nuxt server route) reads the refresh_token httpOnly cookie, exchanges it with Keycloak’s token endpoint (grant_type=refresh_token), and re-sets the access_token, refresh_token, and id_token cookies from the response.
{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"name": "John Doe",
"family_name": "Doe",
"given_name": "John",
"email_verified": true,
"preferred_username": "user@example.com",
"iat": 1234567890,
"exp": 1234571490,
"iss": "http://keycloak:8080/kc/realms/sencai",
"aud": "sencai-frontend",
"typ": "Bearer"
}

Strapi validates via JWKS (JSON Web Key Set) endpoint:

http://keycloak:8080/kc/realms/sencai/.well-known/openid-configuration
http://keycloak:8080/kc/realms/sencai/protocol/openid-connect/certs
  1. User registers via frontend form
  2. Strapi POST /api/registrations
  3. Strapi webhook triggers: POST webhook-publisher:1347
  4. Event: { eventName: 'registration.create', entity: registration, ... }
  5. RabbitMQ → auth-service-consumer
  6. auth-service-consumer calls KC admin API:
    POST /admin/realms/sencai/users
    {
    "username": "550e8400-...", // UUID from KC sub
    "email": "user@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "enabled": true,
    "requiredActions": ["VERIFY_EMAIL"]
    }
  7. KC sends verification email automatically
  8. User clicks link → email verified
  9. Admin in Strapi UI sets Registration.emailVerified = true
  10. Strapi lifecycle hook fires → webhook-publisher
  11. auth-service-consumer removes VERIFY_EMAIL required action
  12. User can log in
  1. User logs in via KC login form
  2. KC issues JWT
  3. Frontend redirects to /auth/callback with code + state
  4. Frontend exchanges code for JWT
  5. Strapi keycloak-jwt middleware validates JWT
  6. Middleware checks Strapi User exists with matching email
  7. If missing name or surname: redirect to /auth/complete-profile
  8. User fills profile → POST /api/auth/complete-profile
  9. Strapi updates User → lifecycle hook → webhook-publisher → KC sync
  10. Strapi User and KC user attributes aligned
  11. Redirect to /gravity/dashboard

Keycloak supports federating external identity providers:

  1. KC Admin → Realm Settings → Identity Providers → Create → Google
  2. Add Google OAuth credentials (client_id, client_secret)
  3. Save
  4. Login page shows Google button
  5. User clicks → redirects to Google login
  6. Google redirects back with access token
  7. KC creates/links user
  1. KC Admin → Identity Providers → Create → SAML v2.0
  2. Upload company’s SAML metadata or configure manually
  3. Configure assertion mapping (email → KC email, etc.)
  4. Company’s SAML IdP redirects to KC assertion consumer URL
  5. KC links user to Sencai account

Keycloak can federate any OIDC provider (Okta, Azure AD, custom):

  1. KC Admin → Identity Providers → Create → OpenID Connect v1.0
  2. Enter Authorization URL, Token URL, Logout URL
  3. Add client credentials
  4. Map claims to KC user attributes
  5. Save
  1. User goes to Account Settings → Security → 2FA
  2. Click Enable 2FA
  3. QR code appears (shows secret + issuer)
  4. User scans in Google Authenticator / Authy / 1Password
  5. User enters 6-digit code from app
  6. KC verifies code and enables TOTP
  7. Recovery codes displayed (save for account recovery)

TOTP uses Speakeasy library:

Required Action: Configure OTP
Algorithm: TOTP (time-based)
Digit: 6
Period: 30s

KC can force users to complete actions:

ActionTriggerDetails
VERIFY_EMAILOn registrationUser must click email verification link
UPDATE_PROFILEAdmin setsUser must fill name/surname on next login
CONFIGURE_OTPAdmin setsUser must set up 2FA
terms_and_conditionsCustomUser must accept T&C

When user logs in with required actions pending, KC redirects to action flow before granting access token.

For auth-service-consumer to manage KC users:

Terminal window
POST http://keycloak:8080/kc/realms/master/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
&client_id=admin-cli
&client_secret=<SECRET>
&username=admin
&password=admin

Response:

{
"access_token": "eyJ...",
"expires_in": 3600,
"token_type": "Bearer"
}

Then use token to call admin API:

Terminal window
POST http://keycloak:8080/kc/admin/realms/sencai/users
Authorization: Bearer eyJ...
{ "username": "user", "email": "user@example.com", ... }

Sencai customizes KC login, registration, and email templates:

Files: auth-service/themes/

  • login/ — Login form HTML/CSS
  • register/ — Registration form
  • email/ — Email templates (verification, password reset)

Upload via KC Admin → Realm Settings → Themes.

Strapi User.afterCreate/Update
→ webhook-publisher (event: user.update)
→ RabbitMQ user.events
→ auth-service-consumer
→ KC admin API: PUT /admin/realms/sencai/users/{id}
→ KC attributes updated

On login, Strapi checks if user profile is complete. If not:

keycloak-jwt middleware
→ fetchKcUserById(decoded.sub)
→ runWithoutKcSync(syncKcUserToStrapi)
→ Strapi User updated from KC
→ No feedback loop (runWithoutKcSync prevents KC update)

Q: KC 503 on startup

A: Keycloak takes 30–90s to start. Wait and retry. Check logs: docker logs keycloak

Q: “invalid algorithm” JWT error

A: Strapi admin panel uses HS256 tokens. Middleware must decode header to check alg and skip non-RS256 tokens.

Q: “Account is not fully set up”

A: User has UPDATE_PROFILE required action. Ensure firstName/lastName are set in KC user attributes.

Q: KC URL redirects with 404

A: Ensure /kc prefix is present: KEYCLOAK_URL=http://keycloak:8080/kc (with trailing /kc).

Q: Token endpoint 404

A: Token endpoint is at /kc/realms/sencai/protocol/openid-connect/token. Verify full URL including /kc prefix.