feat: local Keycloak mock + backend auth env wiring#198
Merged
Conversation
7 tasks
This was referenced May 14, 2026
vredchenko
added a commit
to DiamondLightSource/smartem-frontend
that referenced
this pull request
May 19, 2026
Matches the realm-side rename in smartem-devtools (feat/keycloak-mock, DiamondLightSource/smartem-devtools#198) where the existing browser client is renamed for symmetry with the new SmartEM_Agent service client. See DiamondLightSource/smartem-devtools/decisions/0018 for context.
vredchenko
added a commit
to DiamondLightSource/smartem-decisions
that referenced
this pull request
May 19, 2026
…list, add clock leeway Authentication is now always on - no env-driven opt-out. With the local Keycloak mock in DiamondLightSource/smartem-devtools#198 available for tests and local dev, the flag added more failure modes (silent unauthenticated traffic on misconfig) than it averted. Adds KEYCLOAK_ALLOWED_AZP enforcement so the backend can reject tokens issued to clients outside the expected set (SmartEM_User for the browser, SmartEM_Agent for the agent). Empty value preserves the prior "any valid token from the realm" behaviour. Also adds a 60-second leeway on time-based JWT claims to absorb modest workstation clock skew. Tests that previously relied on auth being off by default now use app.dependency_overrides[verify_token] to inject a stub claims dict. Same treatment in the three test files that define their own client fixture rather than using conftest. A separate real_auth_client fixture exercises the actual verify_token path in test_auth.py. uv.lock updated to pick up pyjwt[crypto]'s transitive cryptography dependency that lands with the backend extra. Towards resolving #284.
vredchenko
added a commit
to DiamondLightSource/smartem-frontend
that referenced
this pull request
May 21, 2026
Matches the realm-side rename in smartem-devtools (feat/keycloak-mock, DiamondLightSource/smartem-devtools#198) where the existing browser client is renamed for symmetry with the new SmartEM_Agent service client. See DiamondLightSource/smartem-devtools/decisions/0018 for context.
vredchenko
added a commit
to DiamondLightSource/smartem-decisions
that referenced
this pull request
May 21, 2026
…list, add clock leeway Authentication is now always on - no env-driven opt-out. With the local Keycloak mock in DiamondLightSource/smartem-devtools#198 available for tests and local dev, the flag added more failure modes (silent unauthenticated traffic on misconfig) than it averted. Adds KEYCLOAK_ALLOWED_AZP enforcement so the backend can reject tokens issued to clients outside the expected set (SmartEM_User for the browser, SmartEM_Agent for the agent). Empty value preserves the prior "any valid token from the realm" behaviour. Also adds a 60-second leeway on time-based JWT claims to absorb modest workstation clock skew. Tests that previously relied on auth being off by default now use app.dependency_overrides[verify_token] to inject a stub claims dict. Same treatment in the three test files that define their own client fixture rather than using conftest. A separate real_auth_client fixture exercises the actual verify_token path in test_auth.py. uv.lock updated to pick up pyjwt[crypto]'s transitive cryptography dependency that lands with the backend extra. Towards resolving #284.
The smartem-frontend Keycloak integration currently has no local
development story — every dev needs `http://localhost:5173` added to
the SmartEM client's Valid Redirect URIs and Web Origins on
`identity-test.diamond.ac.uk`, which is an admin round-trip that has
to be repeated for every new port and every new developer.
A self-contained mock removes that dependency:
- `dls-realm.json` is the single source of truth — realm `dls`,
public client `SmartEM` with PKCE, localhost redirect URIs and
Web Origins, custom `fedId` claim mapper, two seeded users.
- Compose form for the fastest standalone cycle.
- Kustomize form integrated into the existing development overlay,
so `dev-k8s.sh up` now brings up Keycloak alongside the rest of
the stack.
Both forms read the same realm JSON, so editing it once propagates
to whichever form a developer prefers.
The architecture doc gains a "Local development" pointer and the
client-name discrepancy (`smartem-frontend` vs `SmartEM`) is
corrected to match the actual implementation.
…p-api The development environment's `smartem-http-api-service` already claims NodePort 30080. Applying the keycloak-mock kustomization to the development overlay therefore fails with a port conflict. Move the keycloak-external Service to 30090 and update the three doc references that mention 30080. The browser-facing URL becomes `http://<node-ip>:30090`; the ClusterIP path (`http://keycloak-service:8080`) is unchanged.
The backend (smartem-decisions) now consumes KEYCLOAK_AUTH_REQUIRED /
KEYCLOAK_URL / KEYCLOAK_REALM / KEYCLOAK_CLIENT_ID / KEYCLOAK_VERIFY_ISS to
gate Bearer-token validation against a Keycloak realm's JWKS.
- Staging defaults to the real DLS test realm
(https://identity-test.diamond.ac.uk, realm `dls`, client `SmartEM`)
with auth required and iss verification on.
- Development defaults to the in-cluster keycloak-mock service
(http://keycloak-service:8080) with iss verification OFF. Tokens are
minted with the browser-facing URL (localhost:30090) while the pod
reaches Keycloak via in-cluster DNS, so issuer strings don't match
even though the signing key does. Auth itself defaults to disabled
in dev so existing workflows that don't pass a token still work;
flip it on per developer via `.env.k8s.development`.
`dev-k8s.sh ensure_app_configmap()` now also reads the five Keycloak
variables from the environment and bakes them into the dynamically-
generated ConfigMap, so an operator who edits `.env.k8s.staging` and runs
`DEPLOY_ENV=staging ./scripts/k8s/dev-k8s.sh up` gets the right values
without touching YAML.
Examples in `env-examples/.env.example.k8s.{staging,development}` are
updated to document the new variables.
…ent agent auth The SmartEM Agent needs a service-to-service authentication path against the JWT-protected backend. Adds a confidential SmartEM_Agent client to the mock realm (OAuth 2.0 client_credentials grant) and renames the existing browser client SmartEM to SmartEM_User for symmetry in the realm. The choice is recorded in ADR 0018 with the operational details in docs/agent/authentication.md and short cross-links from cli-reference, deployment, troubleshooting, environment-variables and http-api-client. A future upgrade to private_key_jwt client authentication is planned and the agent code is structured so that change requires no behavioural rework. Resolves DiamondLightSource/smartem-decisions#284 (decision).
8803cc1 to
9a1ecf2
Compare
vredchenko
added a commit
to DiamondLightSource/smartem-decisions
that referenced
this pull request
May 21, 2026
…list, add clock leeway Authentication is now always on - no env-driven opt-out. With the local Keycloak mock in DiamondLightSource/smartem-devtools#198 available for tests and local dev, the flag added more failure modes (silent unauthenticated traffic on misconfig) than it averted. Adds KEYCLOAK_ALLOWED_AZP enforcement so the backend can reject tokens issued to clients outside the expected set (SmartEM_User for the browser, SmartEM_Agent for the agent). Empty value preserves the prior "any valid token from the realm" behaviour. Also adds a 60-second leeway on time-based JWT claims to absorb modest workstation clock skew. Tests that previously relied on auth being off by default now use app.dependency_overrides[verify_token] to inject a stub claims dict. Same treatment in the three test files that define their own client fixture rather than using conftest. A separate real_auth_client fixture exercises the actual verify_token path in test_auth.py. uv.lock updated to pick up pyjwt[crypto]'s transitive cryptography dependency that lands with the backend extra. Towards resolving #284.
vredchenko
added a commit
to DiamondLightSource/smartem-frontend
that referenced
this pull request
May 21, 2026
Matches the realm-side rename in smartem-devtools (feat/keycloak-mock, DiamondLightSource/smartem-devtools#198) where the existing browser client is renamed for symmetry with the new SmartEM_Agent service client. See DiamondLightSource/smartem-devtools/decisions/0018 for context.
vredchenko
added a commit
that referenced
this pull request
May 22, 2026
Adds Kubernetes manifests for the smartem-frontend image
produced by smartem-frontend#94 (v0.2.0, now on GHCR). The
image is environment-agnostic: it ships a placeholder
config.json with dev defaults and proxies /api/ to the
backend service via its own nginx, deferring DNS to request
time.
What lands per environment (k8s/environments/<env>/
smartem-frontend.yaml):
- ConfigMap smartem-frontend-config carries the runtime
config.json (Keycloak URL/realm/clientId + authEnabled).
The Deployment subPath-mounts this onto
/usr/share/nginx/html/config.json, overriding the
placeholder shipped in the image.
- Deployment smartem-frontend pulls
ghcr.io/diamondlightsource/smartem-frontend:latest, sets
BACKEND_HOST=smartem-http-api-service so the SPA pod's
nginx proxies /api/ to the backend service.
- Service smartem-frontend-service: NodePort 30100 for
development (next free in the 30000s range; matches the
existing smartem-http-api / Keycloak / RabbitMQ / Postgres
/ Adminer pattern), ClusterIP for staging and production.
Per-environment config.json values:
- development: keycloak.url http://localhost:30090 (the
Keycloak mock NodePort - browser-reachable), authEnabled
false to match KEYCLOAK_AUTH_REQUIRED=false on the dev
backend. Flip to true to exercise the full login chain.
- staging: identity-test.diamond.ac.uk, authEnabled true.
- production: identity.diamond.ac.uk, authEnabled true.
Ingress (staging and production only - dev keeps the
NodePort pattern):
- k8s/environments/{staging,production}/ingress.yaml route
a single host to smartem-frontend-service. The SPA pod's
nginx handles /api/ proxying to the backend internally,
so one route covers everything. Hostnames are placeholders
(smartem-staging.example.com / smartem.example.com) and
flagged TODO until real values are decided.
scripts/k8s/dev-k8s.sh: print the new
http://localhost:30100 (frontend) and http://localhost:30090
(Keycloak, missed when #198 landed) in the access-URLs
section.
Verified locally: kubectl kustomize build is clean for all
three environments. End-to-end browser flow (SPA login,
authenticated /api call) will be exercised on the user's
local k3s after merge.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
Two things had to land together:
http://localhost:5173isn't in theSmartEMclient's redirect URIs onidentity-test.diamond.ac.uk, and getting it added is an admin round-trip per port and per developer.dev-k8s.shneed to carry the sameKEYCLOAK_*settings so the pod can fetch JWKS in-cluster.This PR covers both: a self-contained Keycloak mock for the frontend, and the env-wiring that lets the backend point at it (or at the real DLS realm in staging) without YAML surgery.
What's included
Self-contained Keycloak mock (
keycloak-mock/)Two equivalent deployment forms, both reading from one realm export:
dls-realm.json— single source of truth. Realmdls, public clientSmartEMwith PKCE S256,localhost:5173andlocalhost:5174in redirect URIs and Web Origins, customfedIdclaim mapper to mirror DLS realm claims, two seeded users (devuser/devpass,valuser/valpass).docker-compose.yml— Compose form, fastest standalone cycle (~20 s).keycloak.yaml+kustomization.yaml— Kubernetes Deployment + Services + kustomizeconfigMapGeneratorfor the realm JSON.The Kustomize form is wired into
k8s/environments/development/kustomization.yaml, so./scripts/k8s/dev-k8s.sh upbrings up Keycloak alongside Postgres, RabbitMQ, etc. The Compose form is independent and useful when only the frontend is needed.Backend Keycloak env wiring
smartem-configConfigMap (bothdevelopmentandstagingoverlays) gains:KEYCLOAK_AUTH_REQUIRED— master switch consumed by the backend (defaults:falsein dev,truein staging)KEYCLOAK_URL— dev points athttp://keycloak-service:8080(in-cluster DNS); staging points athttps://identity-test.diamond.ac.ukKEYCLOAK_REALM,KEYCLOAK_CLIENT_ID,KEYCLOAK_VERIFY_ISSDev has
KEYCLOAK_VERIFY_ISS=falsebecause tokens are minted with the browser-facing URL (localhost:30090) while the pod reaches Keycloak via in-cluster DNS — signing key is identical butissstrings differ. Staging hasissverification on.dev-k8s.sh ensure_app_configmap()reads the five vars from the environment and bakes them into the dynamically-generated ConfigMap. Operators only need to edit.env.k8s.<env>— corresponding examples inenv-examples/are updated.Port-conflict fix (
fix(keycloak-mock): move NodePort to 30090)The development environment's
smartem-http-api-servicealready claimed NodePort30080. Applying the dev kustomization with the keycloak mock therefore failed with a port conflict. Movedkeycloak-externalto30090and updated the three doc references.Docs
docs/development/local-keycloak.md— full how-to (when to use which form, how to point the frontend at it, how to edit the realm, limits and non-goals).docs/development/index.md— added to the TOC.docs/architecture/keycloak-spa-authentication.md— "Local development" pointer + corrected thesmartem-frontend→SmartEMclient-name discrepancy.Quick start
Then in
smartem-frontend/apps/smartem/.env.local:Log in as
devuser/devpass.Verified end-to-end
The full chain — SPA -> Keycloak -> SPA with token -> backend
/acquisitions-> backend validates JWT againstkeycloak-service:8080JWKS -> 200 + payload — runs cleanly on a local single-node k3s with all three branches applied (#74, this PR, smartem-decisionsfeat/keycloak-jwt-validation).Test plan
kubectl kustomize keycloak-mockbuilds cleanlykubectl kustomize k8s/environments/developmentbuilds cleanly with Keycloak and KEYCLOAK_* envs includeddocker compose up -dinkeycloak-mock/imports thedlsrealm and exposes the admin console athttp://localhost:8080./scripts/k8s/dev-k8s.sh upbrings Keycloak up alongside the rest of the stack, reachable athttp://<node-ip>:30090KEYCLOAK_AUTH_REQUIRED=truefetches JWKS fromhttp://keycloak-service:8080and accepts a realdevusertoken