Skip to content

feat(local-dev): MIT Learn account actions via a dedicated Keycloak client - #5511

Open
rhysyngsun wants to merge 2 commits into
mainfrom
nl/local-dev-learn-change-email-password-client
Open

feat(local-dev): MIT Learn account actions via a dedicated Keycloak client#5511
rhysyngsun wants to merge 2 commits into
mainfrom
nl/local-dev-learn-change-email-password-client

Conversation

@rhysyngsun

@rhysyngsun rhysyngsun commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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.

  • Adds ol-mitlearn-account-client, a confidential Keycloak client dedicated to Keycloak's application-initiated actions (AIA). It is separate from ol-mitlearn-client, which APISIX uses for the session: Django sends the user to Keycloak with kc_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.
  • Its valid_redirect_uris ends in * so Keycloak's exact-match check still passes once Django appends ?next=...&kc_action=..., mirroring the production mitxonline entry in substructure/keycloak/Pulumi.Production.yaml.
  • Enables the UPDATE_EMAIL required action in the olapps realm. It was registered but left enabled=False, and an AIA for a disabled required action redirects back with kc_action_status=error — which is what the change-email flow was hitting.

The comment being replaced claimed UPDATE_EMAIL was removed in Keycloak 26. It was not. It is gated behind the update-email preview feature, which the mitodl/keycloak image bakes in at build time (--features=update-email in ol-keycloak's Dockerfile.hosted). On the pinned image, kc.sh show-config reports:

kc.features =  update-email (Persisted)

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:

  1. Go to https://learn.mit.dev/dashboard/settings and start the change-email action.
  2. Keycloak should present its update-email form and return you to https://api.learn.mit.dev/account/action/complete?...&kc_action_status=success. Before this change the same URL came back with kc_action_status=error.
  3. Confirm the new address is reflected in Learn.

Confirmed working end-to-end on local-dev.

To verify the realm state directly (local-dev admin credentials are the committed admin/admin defaults used by local-dev/scripts/kc-fix-flow-bindings.sh):

TOKEN=$(curl -s --cacert local-dev/certs/rootCA.pem \
  -X POST "https://sso.ol.mit.dev/realms/master/protocol/openid-connect/token" \
  -d "client_id=admin-cli&username=admin&password=admin&grant_type=password" \
  | jq -r .access_token)

curl -s --cacert local-dev/certs/rootCA.pem -H "Authorization: Bearer $TOKEN" \
  "https://sso.ol.mit.dev/admin/realms/olapps/authentication/required-actions/UPDATE_EMAIL"

UPDATE_PASSWORD was 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.RequiredAction for UPDATE_EMAIL updates 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 up to pick it up.

A keycloak_log_level knob for local-dev is deliberately not in this PR and will follow separately. It is worth having: the olapps realm has events_enabled with the jboss-logging listener, 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.

rhysyngsun and others added 2 commits August 18, 2026 15:50
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>
@rhysyngsun
rhysyngsun marked this pull request as ready for review August 18, 2026 19:53
Copilot AI balanced review requested due to automatic review settings August 18, 2026 19:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_EMAIL required 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 ChristopherChudzicki self-assigned this Aug 20, 2026

@ChristopherChudzicki ChristopherChudzicki left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

# MIT LEARN [START]
if keycloak_realm_config.get("olapps-mitlearn-client-secret"):
olapps_mitlearn_client = keycloak.openid.Client(
"olapps-mitlearn-client",
name="ol-mitlearn-client",
realm_id="olapps",
client_id="ol-mitlearn-client",

?

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.

3 participants