feat(local-dev): MIT Learn account actions via a dedicated Keycloak client - #5511
feat(local-dev): MIT Learn account actions via a dedicated Keycloak client#5511rhysyngsun wants to merge 2 commits into
Conversation
MIT Learn's self-service change email / change password flow drives Keycloak "application initiated actions": Django starts an authorization code flow with a kc_action param, Keycloak walks the user through the form and returns them to /account/action/complete, and Django exchanges the code server-side to read the updated email (the gateway's cached userinfo still holds the old one). That flow reads KEYCLOAK_CLIENT_ID and KEYCLOAK_CLIENT_SECRET, both of which are deliberately defaultless in mit-learn because the callback URL has to be a registered redirect URI on the client. Neither was set anywhere in local-dev, so the account-action endpoint logged an error and bounced the user back with an error alert. Add a dedicated confidential client, ol-mitlearn-account-client, whose only redirect URI is the account-action callback on the Learn API, and wire its id and secret into the mit-learn Deployment's env. Kept separate from ol-mitlearn-client, which APISIX uses for the session via secretRef on every route -- widening its redirect URIs would blur two distinct trust surfaces. Also add the apps-infra stack config to the local-infra-apps Tilt resource's deps. Adding a client always means adding a *_client_secret to that file, and without it Tilt fires on the .py edit alone, fails with "Missing required configuration variable", then never re-runs when the value lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
MIT Learn's change-email flow sends the user to Keycloak with kc_action=UPDATE_EMAIL, and Keycloak was bouncing them straight back to the callback with kc_action_status=error. The action was registered in the realm all along -- the mitodl/keycloak image builds with --features=update-email, so the provider is present (`kc.sh show-config` reports `kc.features = update-email (Persisted)`). It was simply left enabled=False, and an application-initiated action for a disabled required action fails with exactly that status. Declare it alongside the other required actions so it survives a realm rebuild. The comment it replaces claimed UPDATE_EMAIL was removed in Keycloak 26, which is not the case: it is gated behind a preview feature, and that is what a provider "unknown alias" validation error would really be pointing at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds local-development support for MIT Learn’s Keycloak-driven email and password account actions.
Changes:
- Adds and configures a dedicated confidential Keycloak client.
- Enables Keycloak’s
UPDATE_EMAILrequired action. - Wires client credentials into MIT Learn and improves local-dev dependency documentation.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
Tiltfile |
Watches the Pulumi stack configuration for changes. |
local-dev/infra/modules/keycloak.py |
Adds the account-action client and enables email updates. |
local-dev/infra/apps_infra/Pulumi.local-dev.apps-infra.Dev.yaml |
Defines the local client secret. |
local-dev/infra/apps_infra/__main__.py |
Loads and passes the new secret. |
local-dev/EXTENDING.md |
Documents client-secret plumbing. |
local-dev/apps/mit-learn/secrets.yaml |
Supplies the client secret to MIT Learn. |
local-dev/apps/mit-learn/configmaps/app-env.yaml |
Supplies the account-action client ID. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ChristopherChudzicki
left a comment
There was a problem hiding this comment.
As mentioned in Slack, I didn't have SCIM set up, so I didn't get the email updated in Learn. Keycloak did get the update, though.
It seems like some of this PR is built for the time when mitodl/mit-learn#3726 included token exchange. But that got removed, right? So I think we can remove mitlearn_account_client
| # because its only job is Keycloak's application-initiated actions — update | ||
| # email / update password. Django sends the user here with `kc_action`, | ||
| # Keycloak returns them to the callback below, and Django exchanges the code | ||
| # server-side to read the updated email, since the gateway's cached userinfo |
There was a problem hiding this comment.
and Django exchanges the code server-side to read the updated email
@rhysyngsun Am I misunderstanding this PR? Didn't we remove the token exchange?
In that case, can't Learn just stay on ol-mitlearn-client and local-dev/apps/mit-learn/configmaps/app-env.yaml can point KEYCLOAK_CLIENT_ID: ol-mitlearn-client? Isn't that what prod does:
ol-infrastructure/src/ol_infrastructure/substructure/keycloak/olapps.py
Lines 446 to 452 in 6edd20c
?
What are the relevant tickets?
Part of https://github.com/mitodl/hq/issues/10815
Description (What does it do?)
Makes MIT Learn's change-email / change-password account actions work in local-dev.
ol-mitlearn-account-client, a confidential Keycloak client dedicated to Keycloak's application-initiated actions (AIA). It is separate fromol-mitlearn-client, which APISIX uses for the session: Django sends the user to Keycloak withkc_action, Keycloak returns them to/account/action/complete, and Django exchanges the code server-side to read the updated email, since the gateway's cached userinfo still holds the old address.valid_redirect_urisends in*so Keycloak's exact-match check still passes once Django appends?next=...&kc_action=..., mirroring the production mitxonline entry insubstructure/keycloak/Pulumi.Production.yaml.UPDATE_EMAILrequired action in theolappsrealm. It was registered but leftenabled=False, and an AIA for a disabled required action redirects back withkc_action_status=error— which is what the change-email flow was hitting.The comment being replaced claimed
UPDATE_EMAILwas removed in Keycloak 26. It was not. It is gated behind theupdate-emailpreview feature, which themitodl/keycloakimage bakes in at build time (--features=update-emailinol-keycloak'sDockerfile.hosted). On the pinned image,kc.sh show-configreports:So a provider "unknown alias" validation error on that alias would mean the image lost the feature flag, not that the action is obsolete — noted in the new comment so the next person does not re-derive it.
How can this be tested?
Against local-dev (
tilt up), signed in to Learn as a non-SSO user:https://learn.mit.dev/dashboard/settingsand start the change-email action.https://api.learn.mit.dev/account/action/complete?...&kc_action_status=success. Before this change the same URL came back withkc_action_status=error.Confirmed working end-to-end on local-dev.
To verify the realm state directly (local-dev admin credentials are the committed
admin/admindefaults used bylocal-dev/scripts/kc-fix-flow-bindings.sh):UPDATE_PASSWORDwas already enabled, so the change-password action is a useful control that the client and its redirect URI are sound independently of the required-action change.Additional Context
Note for reviewers on how the required action reaches the realm:
keycloak.RequiredActionforUPDATE_EMAILupdates the built-in action that Keycloak already registered rather than creating a new one, which is the same thing the three existing entries (CONFIGURE_TOTP,VERIFY_EMAIL,UPDATE_PASSWORD) do. No Pulumi import is needed.Because the action was flipped live on one local-dev cluster while debugging, that cluster was already in the fixed state before this commit existed. The declaration here is what makes it survive a realm rebuild — anyone else needs the
pulumi upto pick it up.A
keycloak_log_levelknob for local-dev is deliberately not in this PR and will follow separately. It is worth having: theolappsrealm hasevents_enabledwith thejboss-logginglistener, but Keycloak emits those events at DEBUG, so this failure produced nothing in the pod log at the default level and had to be diagnosed from the realm's required-action state instead.