Suncoast Systems Keycloak Auth server
manifests/02-keycloak-configmaps.yaml defines a shared public OIDC client for browser apps:
- client id:
shell-spa-public - realm:
external - issuer URL:
https://auth.suncoast.systems/realms/external - discovery URL:
https://auth.suncoast.systems/realms/external/.well-known/openid-configuration
This client is configured for Authorization Code + PKCE (S256) and includes redirect/web-origin patterns for:
http://localhost/*http://127.0.0.1/*https://*.suncoast.systems/*https://*.app.suncoast.systems/*
The dedicated auth gateway source now lives in:
https://github.com/suncoast-systems/keycloak-auth-gateway
This repo keeps the deployment manifest at manifests/07-auth-gateway.yaml.
The Keycloak external realm client auth-gateway-public is defined in manifests/02-keycloak-configmaps.yaml.
Purpose:
- Centralize Keycloak redirect handling to a single callback URL (for example
https://login.suncoast.systems/callback). - Store allowed apps in a database so a GUI can manage them dynamically.
- Expose APIs for CRUD management of allowed apps.
Database tables created automatically at startup:
auth_gateway_allowed_appsauth_gateway_login_stateauth_gateway_exchange_codes
Runtime flow:
- App sends users to
GET /start?app=<slug>&return_to=<url-or-path>. - Gateway redirects to Keycloak and receives callback at
GET /callback. - Gateway redirects back to app with one-time
gateway_codequery param. - App exchanges that code with
POST /v1/auth/exchange.
Allowed app URL policy:
- Apps now support
base_urls(list of allowed URLs) in addition to legacybase_url. - This allows one app slug to support multiple trusted origins (for example localhost + preview + prod) without wildcard matching.
Management APIs (for GUI):
GET /v1/appsPOST /v1/appsGET /v1/apps/{slug}PUT /v1/apps/{slug}DELETE /v1/apps/{slug}
GitOps sync for app registrations:
manifests/10-auth-gateway-sync-apps.yaml- Bootstrap Job + CronJob reconcile
auth_gateway_allowed_appsfrom the registry service API.
Management APIs require:
Authorization: Bearer <ADMIN_API_TOKEN>ADMIN_API_TOKENis sourced from Vault KVv2 pathsecret/data/auth-gateway-admin-api, keyvalue(synced into Kubernetes Secretauth-gateway-admin-apiby External Secrets).
Build/publish is managed in the keycloak-auth-gateway repo via GitHub Actions.
Deployment in this repo expects:
ghcr.io/dotcomrow/keycloak-auth-gateway:latest
kubectl apply -f manifests/07-auth-gateway.yaml
kubectl apply -f manifests/10-auth-gateway-sync-apps.yamlRequired Vault secret (KVv2 path secret/data/keycloak-client-secret-auth-gateway-exchange):
vault kv put secret/keycloak-client-secret-auth-gateway-exchange value='<strong-client-secret>'Required Vault secret:
vault kv put secret/auth-gateway-admin-api value='<strong-admin-token>'Optional secret:
kubectl -n keycloak create secret generic auth-gateway-oidc-client \
--from-literal=client-secret='<client-secret-if-using-confidential-client>'APISIX example route:
manifests/08-auth-gateway-apisix-route.example.yaml
manifests/04-keycloak-configurator.yaml configures IdPs and imports as much profile data as is available from
external identity providers into Keycloak user attributes. Apps then receive these fields via an OIDC client
scope named user-profile (added as a default client scope for the main app clients in both realms).
The configurator also reconciles realm users/profile metadata so these attributes are visible in the Keycloak
Account Console Personal Info page. The account UI only renders attributes explicitly present in users/profile,
so custom IdP fields must be defined there.
IdP attribute mappers are reconciled on every configurator run and use syncMode=FORCE for profile fields.
picture(avatar URL)profile(profile URL)websitelocalenamegiven_namefamily_namepreferred_usernameemail_verified
- Google:
hd,google_sub - GitHub:
company,location,bio,twitter_username,github_id,github_node_id
Notification contact notes:
- Phone and device contact claims are in optional scope
notification-contact(not default). phone_number/phone_number_verifiedare emitted only when this scope is requested and the upstream/user profile has values.device_idis emitted from the Keycloak user attributedevice_idwhen present.- Google IdP is configured with
openid email profile phoneso OIDC phone claims can be imported when available. - GitHub's standard
/userpayload does not include a phone field, so internal users typically need phone populated from another upstream source or pre-set user attributes. - Existing users may need to authenticate through their IdP again after mapper/scope changes for new attribute values to hydrate into Keycloak user attributes.
- Tokens/userinfo include the above attributes as claims when the client has the
user-profilescope attached. - To read phone/device contact claims, request optional scope
notification-contactin the OIDCscopeparameter. - The configurator also ensures the standard
emailscope is present for app clients that expectemail.
For a fast cross-platform decision, evaluate notification capability from token/userinfo claims as follows:
can_email:emailpresent AND (email_verifiedistrueOR your policy allows unverified email)can_sms:phone_numberpresent ANDphone_number_verifiedistruecan_voice_call:phone_numberpresent (and optionallyphone_number_verifiedby policy)can_voicemail: same ascan_voice_callcan_push:device_idpresent
Recommended implementation pattern:
- Request
notification-contactonly for apps/features that need contact channels. - If
phone_number/device_idclaims are absent when scope was requested, treat the channel as unavailable. - Return a normalized capability object from your auth/profile service so all apps use the same decision logic.
This repo includes a Cloudflare Tunnel deployment at manifests/06-cloudflare-tunnel.yaml.
It runs cloudflared in the keycloak namespace and reads TUNNEL_TOKEN from a Kubernetes Secret named cloudflare-tunnel-token.
That Secret is created by External Secrets using Vault.
Vault policy + role for External Secrets (externalsecrets-keycloak) are created by:
manifests/03-vault-bootstrap-jobs.yaml
- Create a named tunnel in Cloudflare Zero Trust (or with the CLI) and copy the tunnel token.
cloudflared tunnel login
cloudflared tunnel create keycloak
cloudflared tunnel route dns keycloak auth.suncoast.systems
cloudflared tunnel token keycloak- Write the token into Vault (KVv2) at
secret/data/keycloak-cloudflare-tunnel-tokenwith keyvalue.
vault kv put secret/keycloak-cloudflare-tunnel-token value='<PASTE_TUNNEL_TOKEN>'- Sync ArgoCD so these manifests are applied:
manifests/03-vault-bootstrap-jobs.yamlmanifests/06-cloudflare-tunnel.yaml
- In Cloudflare Zero Trust, set the tunnel public hostname and origin service:
- Hostname:
auth.suncoast.systems - Service URL:
http://keycloak.keycloak.svc.cluster.local:8080
kubectl -n keycloak get deploy,pod -l app=cloudflared
kubectl -n keycloak logs deploy/cloudflared --tail=100 -f
kubectl -n keycloak get externalsecret cloudflare-tunnel-token