Skip to content

feat: Microsoft Entra authentication for Temporal (server JWT + UI OIDC)#89

Merged
bmorton merged 21 commits into
mainfrom
feat/entra-authentication
Jun 25, 2026
Merged

feat: Microsoft Entra authentication for Temporal (server JWT + UI OIDC)#89
bmorton merged 21 commits into
mainfrom
feat/entra-authentication

Conversation

@bmorton

@bmorton bmorton commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Wires Microsoft Entra into Temporal for both service-to-service JWT auth and
human UI login, building on the passwordless Entra work from #85. Stock
temporalio/server and temporalio/ui images throughout — no custom claim
mapper or image.

What's included

Server JWT auth (spec.authorization)

  • Typed entra.tenantID shortcut that derives the Entra JWKS URL
    (https://login.microsoftonline.com/<tenant>/discovery/v2.0/keys), plus
    explicit jwtKeyProvider.keySourceURIs / refreshInterval and
    permissionsClaimName (defaults to roles when entra is set — Entra app
    roles land in the roles claim).
  • authorizer is now a *string: unset → defaults to "default"
    (per-namespace RBAC) when JWT is configured; "" → Temporal's no-op
    authorizer for authenticate-only mode. Both modes are first-class.
  • The previously-dead authorization.config passthrough is now rendered (and
    cleanly overrides modeled keys, incl. jwtKeyProvider, with no duplicate YAML).

UI OIDC login (spec.ui.auth)

  • Replaces the unused ui.auth raw passthrough with a typed UIAuthSpec:
    enabled, entra.tenantID, providerURL, clientID, clientSecretRef,
    scopes, callbackURL, extraEnv — rendered as TEMPORAL_AUTH_* env on the
    UI Deployment. The client secret is sourced via secretKeyRef and never
    inlined.

Validation, docs, example, tests

  • Webhook validation of ui.auth (required fields; exactly one provider source;
    extraEnv must be a string map).
  • New guide: docs/content/operations/authentication.md (authn-only + RBAC via
    Entra app roles, UI OIDC, the longstanding app, security caveats),
    cross-linked from the Azure install guide. Regenerated CRD/API reference,
    mirrored into dist/chart.
  • Example examples/cluster-entra-auth/ using a 1Password OnePasswordItem for
    the UI client secret.
  • Hermetic JWKS server-JWT e2e (test/e2e/entra-auth/, Chainsaw on kind, no
    Azure): an offline RSA keypair + static JWKS, baked valid/invalid JWTs, proves
    a valid <namespace>:<role> token is authorized and missing/wrong-signature
    tokens are rejected — with the denial checks gated on the validator being
    demonstrably live.
  • hack/entra-e2e.sh (+ make entra-e2e-up/-token/-down) creates a longstanding
    Entra app registration (app roles default:read/default:write/
    temporal-system:admin) for manual real-Entra verification.

Breaking change

spec.ui.auth changes from raw YAML to a typed object. The field was previously
dead (never rendered), and the project is pre-1.0.

Validation

make build, make test, make lint (0 issues), and make generate manifests
(no drift) all pass. The hermetic e2e suite's crypto is verified offline; the
live kind run is documented (not run in this environment).

Design + plan:
docs/superpowers/specs/2026-06-21-entra-authentication-design.md,
docs/superpowers/plans/2026-06-21-entra-authentication.md.

bmorton and others added 21 commits June 24, 2026 08:14
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Replaces the unused UISpec.Auth passthrough with a typed UIAuthSpec.

BREAKING CHANGE: spec.ui.auth is now a typed object, not raw YAML.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Split 03-authz-checks.yaml into two sequential phases:
- 03-authz-allow.yaml: ConfigMap (valid + invalid tokens) + authz-allow Job
- 04-authz-deny.yaml: authz-deny Job + new authz-bad-token Job

chainsaw-test.yaml gains two steps (authz-allow, authz-deny) replacing
the single authz-checks step.  The denial step is only reached after
authz-allow succeeds, proving the JWKS endpoint was fetched and the
validator is live — closing the fail-open race window.

authz-bad-token exercises token-invalid.txt (valid structure, wrong
signature) in-cluster, proving signature verification and not just
missing-auth rejection.  backoffLimit:0 on both denial Jobs prevents
retries masking false positives.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Change AuthorizationSpec.Authorizer from string to *string so an
explicit empty value is distinguishable from unset. Add EmitAuthorizer
bool to AuthConfig and update buildAuth to default to "default" only
when the field is nil (unset). Update the config template to gate
authorizer emission on EmitAuthorizer rather than a non-empty string
check, so authorizer: "" renders correctly.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Update CRD description, API reference docs, and Helm chart CRD to
reflect the new Authorizer *string doc comment.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
…ons, passthrough)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Brian Morton <brian@mmmhm.com>
@bmorton
bmorton force-pushed the feat/entra-authentication branch from db987af to 918c372 Compare June 24, 2026 19:09
@bmorton
bmorton merged commit d674939 into main Jun 25, 2026
14 checks passed
@bmorton
bmorton deleted the feat/entra-authentication branch June 25, 2026 05:53
bmorton added a commit that referenced this pull request Jun 25, 2026
The Microsoft Entra fields added in #89 committed a dist/chart CRD whose
descriptions were wrapped by a different generation process. The pinned
'make helm-chart' (kubebuilder helm/v2-alpha) emits single-line
descriptions, so the verify-chart CI check added in #98 flagged the
artifact as stale. Regenerate dist/chart deterministically; the change is
purely description re-wrapping with no semantic difference.

Signed-off-by: Brian Morton <brian@mmmhm.com>
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