Skip to content

feat(keycloak): techgarden login + email theme on dev (WS-05) - #275

Merged
TechGardenCode merged 2 commits into
mainfrom
feat/ws-05-keycloak-techgarden-theme
Aug 5, 2026
Merged

feat(keycloak): techgarden login + email theme on dev (WS-05)#275
TechGardenCode merged 2 commits into
mainfrom
feat/ws-05-keycloak-techgarden-theme

Conversation

@TechGardenCode

Copy link
Copy Markdown
Owner

Infra half of cross-repo workstream WS-05. App half: TechGardenCode/techgarden#181 (feature/ws-05-keycloak-login-theme), brief at docs/cross-repo/ws-05-keycloak-login-theme.md.

Dev only. Prod has no techgarden realm; WS-02 derives the prod seed from this.

Route taken: ConfigMap (as briefed), with one contract delta

Delta — two ConfigMaps, not one. The brief specified a single ConfigMap with 18 items[]. That isn't expressible: configMapGenerator keys off the file's base name and ConfigMap keys can't contain /, and the tree has two basename collisions — theme.properties (in both login/ and email/) and messages_en.properties (in both login/messages/ and email/messages/). Splitting per theme type removes the collision without key renaming, and drops items[] to 11 + 6 = 17. The 18th file is README.md — docs, lives beside the theme, not mounted.

Zero theme bytes changed. All 18 files verified byte-identical to TechGardenCode/techgarden@79b6b81 by git blob SHA.

Theme location

kubernetes/clusters/1276-dev/keycloak/keycloak/base/themes/
├── README.md
└── techgarden/{login,email}/…

The three confirmations

  1. spec.unsupported.podTemplate — accepted. Confirmed present in the vendored base/operator/crd-keycloaks.yaml @26.6.3, and confirmed as the only volume route. Tech Preview is tolerable here because the operator is version-pinned and vendored in-repo, so merge semantics can't shift under an unplanned upgrade. The custom-image fallback is rejected for the brief's reasons plus one it didn't have: homelab image tags are bot-managed via ci/app-registry.yaml and a PreToolUse hook blocks hand edits, so a hand-built Keycloak image needs a publish path that doesn't exist.
  2. ConfigMap mechanics — fine, measured not estimated. login CM serializes to 336,182 B (32.1% of the 1 MiB cap), email to 12,932 B. binaryData auto-selected for the 4 woff2 + .ico + .png; the 2 OFL licences and login.css stayed UTF-8 in data.
  3. Pod roll — acknowledged and wired. generatorOptions.disableNameSuffixHash: true is global in this kustomization (config-cli mounts the realm CM by fixed name), so theme CMs get stable names and a theme edit alone won't roll the pod. And Keycloak caches themes 30 days in production mode, so a roll is required regardless of naming. Added techgarden.gg/theme-revision on the podTemplate: bump it on any change under base/themes/ and ArgoCD delivers the roll — no out-of-band kubectl. This PR's own rollout is forced by the volume change itself.

Action-token lifespan

The brief pointed at the wrong field. actionTokenGeneratedByAdminLifespan was already 259200 (72h), so admin-issued execute-actions-email invites were never the 5-minute ones. The 5 minutes observed app-side is actionTokenGeneratedByUserLifespan (Keycloak default 300s), which drives self-service verify-email and forgot-password.

Settled on: pin the base at 300, override verify-email only to 43200 (12h) via realm attributes. A friend's verify link survives the wait; the credential-reset window doesn't widen 144×. Live realm attributes was {}, so the addition is purely additive.

smtpServer / WS-04 Resend transport untouched.

Validation

  • kustomize build kubernetes/clusters/1276-dev/keycloak/keycloak — pass
  • kubectl apply --server-side --dry-run=server against the live dev CRD — pass (CR + both ConfigMaps)
  • python3 -m json.tool on the realm seed — valid
  • Mount target verified in the running pod: /opt/keycloak/themes/ contains only README.md, so the mount shadows nothing and parent=base keeps resolving
  • Generated output asserted: container keycloak, 2 mounts, 11 + 6 items[], binaryData on the 6 binaries
  • Confirmed deep dirs don't spawn stray Applications — base/realms/ and base/operator/ already exist and generate none

Post-merge: config-cli PostSync applies the realm fields, and the podTemplate change rolls keycloak-0.

🤖 Generated with Claude Code

https://claude.ai/code/session_01S5ph2xAnD7oCR8YmFmobDn

Theme tree taken verbatim from TechGardenCode/techgarden@79b6b81,
docs/cross-repo/ws-05-theme/ — 18 files, 265,124 B, all verified
byte-identical by git blob SHA. Authored and proven app-side against
quay.io/keycloak/keycloak:26.6.3 (six login surfaces driven in a
browser light+dark, three emails inspected, zero outbound requests,
a11y measured).

This is the theme's living home; the app-repo copy is transit only
and is deleted at WS-05 stage 3. Do not hand-edit login.css,
theme.properties or the message bundles — every value is copied from
the app repo's design-system/, which carries a drift callback here.

Wiring lands in the next commit.

Refs: WS-05, TechGardenCode/techgarden#181

Claude-Session: https://claude.ai/code/session_01S5ph2xAnD7oCR8YmFmobDn
Mounts the vendored theme into the operator-managed Keycloak and turns
it on for the dev techgarden realm. Dev only — prod has no techgarden
realm; WS-02 derives the prod seed from this.

kustomization.yaml
  Two configMapGenerator entries, not one. configMapGenerator keys off
  the file's BASE name and ConfigMap keys cannot contain "/", so a
  single ConfigMap collides: theme.properties and
  messages_en.properties each exist under both login/ and email/.
  Splitting per theme type removes the collision without renaming keys.
  woff2/ico/png land in binaryData automatically; the login ConfigMap
  serializes to 336 KB (32% of the 1 MiB cap), email to 13 KB.

base/keycloak-cr.yaml
  spec.unsupported.podTemplate mounts both ConfigMaps at
  /opt/keycloak/themes/techgarden/{login,email}, with items[] rebuilding
  the subdirectories configMapGenerator flattened. KeycloakSpec @26.6.3
  exposes no supported volume field, so this Tech Preview escape hatch
  is the only ConfigMap route; accepted because the operator is
  version-pinned and vendored in base/operator/. Verified in the running
  pod that /opt/keycloak/themes/ holds only README.md, so the mount
  shadows nothing and parent=base keeps resolving.

  Adds a techgarden.gg/theme-revision annotation. Keycloak caches themes
  for 30 days in production mode and the operator does not watch these
  ConfigMaps, so theme bytes are not served until the pod rolls — and
  the ConfigMaps have stable names, so nothing else forces one. Bump the
  annotation on any change under base/themes/.

base/realms/techgarden-realm.json
  loginTheme + emailTheme = techgarden.

  Also pins the action-token lifespan. The invite path was already fine:
  actionTokenGeneratedByAdminLifespan is 259200 (72h), so admin-issued
  execute-actions-email links were never the 5-minute ones. The 5 minutes
  observed app-side is actionTokenGeneratedByUserLifespan (Keycloak
  default 300s), which drives self-service verify-email AND
  forgot-password. Pinned at 300 with a verify-email-only override to
  43200 (12h) via realm attributes, so a friend's verify link survives
  the wait without widening the credential-reset window 144x. Live realm
  attributes were empty, so this is purely additive.

  smtpServer / Resend transport untouched — emailTheme changes templates,
  not transport.

Refs: WS-05, TechGardenCode/techgarden#181

Claude-Session: https://claude.ai/code/session_01S5ph2xAnD7oCR8YmFmobDn
@TechGardenCode
TechGardenCode merged commit 5e22317 into main Aug 5, 2026
6 checks passed
@TechGardenCode
TechGardenCode deleted the feat/ws-05-keycloak-techgarden-theme branch August 5, 2026 01:04
TechGardenCode added a commit that referenced this pull request Aug 5, 2026
…05) (#276)

Forgot-password links in the dev techgarden realm expired in 5 minutes.
Keycloak resolves the reset-credentials token from the base
actionTokenGeneratedByUserLifespan, which #275 pinned at 300 — so the
verify-email override landed but the reset flow kept the default.

That 300 was a decision, but a badly framed one: it was chosen against
43200 as the only alternative, on the grounds that a 144x wider window
for credential reset buys nothing. A middle value was never considered.
1800 is 6x, sits in the normal band for password-reset tokens, and makes
the flow WS-04 exists to provide actually usable — request a reset, go
find your phone, open the mail six minutes later, and the link still
works.

Per-action override rather than raising the base, so verify-email keeps
12h and any future user-initiated action still inherits Keycloak's
conservative 300s unless deliberately overridden.

Realm-only change: reconciled by the config-cli PostSync hook via the
Admin API. No theme bytes touched, so no theme-revision bump and no pod
roll needed.

Reported by the WS-05 orchestrator during stage 3 joint validation.

Refs: WS-05, TechGardenCode/techgarden#181, #275

Claude-Session: https://claude.ai/code/session_01S5ph2xAnD7oCR8YmFmobDn
TechGardenCode added a commit that referenced this pull request Aug 5, 2026
…277)

The forgot-password success alert was still stock Keycloak. Found by the
orchestrator during stage-3 live validation, not reachable locally:
info.ftl never renders in that flow — Keycloak returns the same response
whether or not the account exists, as an alert back on the sign-in page.

login/messages/messages_en.properties copied verbatim from the app repo
at 24af3ca8 (blob d7fc75a). One key added, emailSentMessage, plus its
comment; the wording still does not confirm the account exists, so the
anti-enumeration property is preserved. Diffed the whole tree: the other
17 files are byte-identical, so nothing else moved.

Bumps techgarden.gg/theme-revision 1 -> 2. The theme ConfigMaps have
stable names and Keycloak caches themes for 30 days in production mode,
so without this the ConfigMap would update and the old bytes would keep
being served.

Refs: WS-05, TechGardenCode/techgarden#181, #275

Claude-Session: https://claude.ai/code/session_01S5ph2xAnD7oCR8YmFmobDn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant