Phase 1: Keycloak login with Authorization Code + PKCE (five test accounts) #115
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Type
Feature
Lineage
Child of #107 (Auth + Roles + Audit Trail). First of five phases defined in
docs/user-stories-auth.md(PR #120, updated in PR #131).Spike #132 validated architecture: standard flow (not ROPC), Terraform-managed realm via pal-e-services.
Repo
ldraney/landscaping-assistantUser Story
As Lucas, I want to log in with a username and password so the app knows who I am and stops being publicly accessible.
Context
The app is currently wide open -- anyone on Tailscale can view and modify property data. This ticket adds basic login/logout as the foundation for multi-user auth. Five test accounts cover all five roles (super_admin, admin, lead, member, client) so we can verify the permission model in later phases without needing real users yet.
Keycloak setup (Terraform via pal-e-services, per spike #132):
landscaping(separate fromwestside-basketball-- added tok3s.tfvars)landscaping-assistantwith Standard flow + PKCE S256 (no ROPC -- Direct Access Grants disabled per SOP and OAuth 2.1)landscaping-assistantnamespace must be added to Keycloak allowlist inpal-e-platform/terraform/network-policies.tflandscapingrealm (created via admin console aftertofu apply):lucas-super-admin(roles:admin+super_admin)lucas-admin(role:admin)lucas-lead(role:lead)lucas-crew(role:member)lucas-client(role:client)admin,lead,member,client,super_adminAuth flow (Authorization Code + PKCE):
/auth/keycloak/callbackwith auth codeturbo-ios compatibility:
landscapingtheme to match app visual identityASWebAuthenticationSessionfor native OIDC flow if webview is insufficientNot in scope (later phases): role enforcement, per-user queues, PaperTrail, user management UI.
Prerequisites (separate tickets/PRs)
landscapingrealm +landscaping-assistantclient tok3s.tfvars, runtofu applylandscaping-assistantnamespace to Keycloak NetworkPolicy allowlistlandscaping-assistant-secretsk8s secretFile Targets
app/controllers/sessions_controller.rb(new -- OmniAuth callback handler)app/controllers/application_controller.rb(addauthenticate_user!before_action)config/initializers/omniauth.rb(new -- OmniAuth OIDC strategy config)config/routes.rb(add/auth/keycloak/callback,/logout)app/views/layouts/application.html.erb(logout button)Gemfile(addomniauth_openid_connectgem)spec/requests/sessions_spec.rb(new)spec/support/auth_helper.rb(new -- test helper for authenticated requests)Acceptance Criteria
landscapingrealm exists in Keycloak with five test users and five realm roleslandscaping-assistantclient exists (confidential, standard flow + PKCE, no ROPC)Test Expectations
spec/requests/sessions_spec.rb: OmniAuth callback success, callback failure, logout, redirect-after-loginspec/support/auth_helper.rb: helper to mock OmniAuth auth hash in request specsauthenticate_user!lands)bundle exec rspecConstraints
omniauth_openid_connectgem (or equivalent) for the OIDC flowkeycloak_loginflag (per docs/feature-flags.md)Checklist
Related
docs/user-stories-auth.md-- five-role model, permission matrixdocs/keycloak-setup.md-- spike output with realm design and setup instructionsdocs/feature-flags.md-- keycloak_login flagsop-keycloak-client-creation-- security baselines (PKCE, no ROPC, etc.)ldraney referenced this issue2026-06-06 11:13:47 +00:00
Phase 1: Keycloak login for single user (three test accounts)to Phase 1: Keycloak login for single user (four test accounts)Issue #115 Template Review
TEMPLATE CONFORMANCE
### Typeheader present and valid (Feature)### Lineagepresent and populated -- references parent #107 and source doc### Repopresent and populated (ldraney/landscaping-assistant)### User Storypresent and follows As/I want/So that format### Contextpresent and thorough -- includes Keycloak setup details, Rails auth flow, and explicit scope exclusions### File Targetspresent with specific file paths and descriptions### Acceptance Criteriapresent with checkboxes (- [ ]format)### Test Expectationspresent with specific spec file names and run command### Constraintspresent with clear patterns and boundaries### Checklistpresent with checkboxes### Relatedpresent with issue references and doc referencesAll 11 required sections present, non-empty, and well-formed. No template conformance issues.
CONTENT QUALITY
File Targets -- verified against repo:
All file paths are accurate. Files marked as new (
sessions_controller.rb,keycloak_auth_service.rb,sessions/new.html.erb, both spec files) do not exist yet. Files marked as modify (application_controller.rb,routes.rb,application.html.erb,Gemfile) exist and are correctly identified. Theapp/services/directory does not exist yet either -- the implementing agent will need to create it. This is implicit but obvious, so not a blocker.Acceptance Criteria -- testable and complete:
All 11 criteria are specific and verifiable. Good coverage of happy path (login, redirect, session), error path (failed login), and infrastructure (realm, client, env vars). The "user returns to originally requested page" criterion is a nice catch for redirect-after-login behavior.
Test Expectations -- realistic:
Spec files are named correctly per Rails conventions. The note about existing request specs needing a session auth helper is critical and shows awareness of the blast radius -- adding
authenticate_user!as abefore_actionwill break every existing request spec. Good that this is called out explicitly.Constraints -- clear and non-contradictory:
The constraints are well-scoped: username-only login, no local User model, session-based auth, Turbo Native compatibility. The reference to basketball-api's ROPC pattern gives the implementing agent a concrete example to follow.
Related -- verified:
docs/user-stories-auth.mdexists and references this ticket at line 393sop-keycloak-client-creationis a pal-e-docs reference (external, not verified here)Phase 1 alignment:
The ticket scope matches the Phase 1 definition in
docs/user-stories-auth.md(lines 346-349) exactly: Keycloak realm + client, four test users, login/logout/redirect. No scope creep into Phase 2 (role enforcement, tab visibility).Implementability:
The ticket is implementable by a dev agent with minimal ambiguity. Two observations:
Keycloak realm pre-requisite: The Context section states Keycloak setup is "manual, per sop-keycloak-client-creation." The Checklist also lists "Keycloak realm + client + users created (manual)" and "K8s secret updated with Keycloak env vars (manual)." This is correctly scoped -- the agent codes the Rails side, and the manual steps are clearly separated. However, the ticket does not specify the order dependency: the manual Keycloak steps must be done BEFORE the agent can integration-test anything. Since the Test Expectations say to stub HTTP calls to Keycloak in the service spec, this is fine for CI, but worth noting for anyone doing manual verification.
JWT gem: The Gemfile target correctly identifies that a
jwtgem needs to be added. The current Gemfile has no JWT dependency. The agent will need to pick betweenjwt(ruby-jwt) and potentiallyjson-jwt-- the constraint to follow basketball-api's pattern should clarify this, but the ticket could be more explicit about which gem.Case-insensitive username: The constraint says "case-insensitive matching," but since there is no local User model and Keycloak is the sole user store, case sensitivity depends on the Keycloak realm configuration. The agent needs to either (a) downcase the username before sending to Keycloak's token endpoint, or (b) ensure the Keycloak realm is configured for case-insensitive usernames. This is an edge case worth noting but not a blocker since the constraint is stated and the agent can handle it.
BLOCKERS
None.
NITS
Gem name specificity: The File Targets say "add jwt gem" but do not specify the exact gem name. Ruby has
jwt(ruby-jwt) andjson-jwt. Consider specifyinggem "jwt"explicitly to remove ambiguity.Successful login redirect target: The Acceptance Criteria says "Successful login redirects to Today tab." Currently
rootpoints towork_queue_items#index(the Today tab), so this is correct for now. But Phase 2 will change the default tab per role (Admin -> Crew, Client -> My Property perdocs/user-stories-auth.mdlines 65-68). A brief note acknowledging that the redirect target will change in Phase 2 would help future readers understand why it's hardcoded to Today now.Missing "do not touch" list: The File Targets template encourages listing "Files the agent should NOT touch." This section is absent. For a ticket that adds
before_action :authenticate_user!toApplicationController, it might be worth noting that individual controllers (e.g.,properties_controller.rb,work_queue_items_controller.rb) should NOT get their own auth logic -- theApplicationControllerapproach covers them all.Checklist ordering: The manual Keycloak steps are listed first in the Checklist, which correctly reflects dependency order. No change needed, just noting this is well done.
VERDICT: APPROVED
ldraney referenced this issue2026-06-06 21:29:10 +00:00
Phase 1: Keycloak login for single user (four test accounts)to Phase 1: Keycloak login with Authorization Code + PKCE (five test accounts)Cross-Repo Prerequisites — Starting 2026-06-06
Working through these in order. Each step depends on the previous.
Step 1: pal-e-services — Terraform realm + client
landscapingrealm tokeycloak_realmsink3s.tfvarslandscaping-assistantclient tokeycloak_clientsink3s.tfvarstofu plan→tofu applycurl https://keycloak.tail5b443a.ts.net/realms/landscaping/.well-known/openid-configurationStep 2: pal-e-platform — NetworkPolicy
landscaping-assistantnamespace to Keycloak ingress allowlist innetwork-policies.tftofu applyStep 3: Keycloak admin console — 5 test users
lucas-super-admin(admin + super_admin),lucas-admin,lucas-lead,lucas-crew,lucas-clientStep 4: k8s secrets
KEYCLOAK_URL,KEYCLOAK_REALM,KEYCLOAK_CLIENT_ID,KEYCLOAK_CLIENT_SECRETtolandscaping-assistant-secretsStep 5: pal-e-deployments — env vars
deployment-patch.yaml(initContainer + container)Step 6: Rails auth (this repo)
omniauth_openid_connectgem, OmniAuth initializer, sessions controller, feature-flagged behindkeycloak_loginWill comment progress on each step.
Step 1 DONE: pal-e-services Terraform
PR pal-e-services#104 merged.
landscapingrealm andlandscaping-assistantclient added tok3s.tfvars.example.Next: copy entries to real
k3s.tfvarsand runtofu plan/tofu applyon archbox. Then Step 2 (NetworkPolicy).Step 1 VERIFIED: Terraform applied
tofu applycomplete. Realm is live:8 resources created: 1 realm, 5 roles (admin, lead, member, client, super_admin), 1 client (landscaping-assistant), 1 realm-roles protocol mapper.
Moving to Step 2 (NetworkPolicy) and Step 3 (test users).
Step 3 DONE: Test users created
5 test users created via Keycloak Admin REST API with roles assigned:
Password:
Test1234!(all users, non-temporary)Step 2 (NetworkPolicy) agent is still running.
Step 4 DONE: k8s secrets updated
landscaping-assistant-secretsnow has 12 keys (8 existing + 4 new):KEYCLOAK_URL=https://keycloak.tail5b443a.ts.netKEYCLOAK_REALM=landscapingKEYCLOAK_CLIENT_ID=landscaping-assistantKEYCLOAK_CLIENT_SECRET= (from Keycloak Credentials tab)Step 2 (NetworkPolicy) QA review in progress. Step 5 (deployment env vars) agent running.
Step 2 DONE: NetworkPolicy applied
PR pal-e-platform#414 merged and
tofu applycomplete.landscaping-assistantnamespace is in the Keycloak ingress allowlist. Pods can reach Keycloak.Waiting on step 5 (pal-e-deployments env vars PR) — agent still running.
Step 5 DONE: Deployment env vars merged
PR pal-e-deployments#188 merged. ArgoCD will sync the 4 Keycloak env vars to the pod.
All prerequisites complete
Ready for Step 6: Rails auth implementation (OmniAuth) in this repo.
Step 6 DONE: Rails OmniAuth implementation
PR #134 opened: Add Keycloak OmniAuth login with Authorization Code + PKCE
Branch:
115-keycloak-omniauth-login— 10 files, 398 additions.What's in the PR:
omniauth_openid_connect(> 0.8) +> 1.0)omniauth-rails_csrf_protection(SessionsController— callback extracts username/email/realm_access roles into session, logout redirects through Keycloak end-session endpointApplicationControllerhelpers:current_user,logged_in?,authenticate_user!,current_user_has_role?,keycloak_configured?Auth NOT enforced globally — login/logout UI appears but all routes remain open. Role-gated tabs come in Phase 2.
QA review in progress.