Repository navigation
mz: Use OAuth authorization code with PKCE for browser login - #40319
Merged
Merged
Conversation
jubrad
marked this pull request as ready for review
October 28, 2026 21:31
Add a browser login to `mz profile init` that uses the admin endpoint's OAuth 2.0 authorization server directly: the authorization code flow for native apps (RFC 8252) with PKCE (RFC 7636). `mz` registers a public client through dynamic client registration (RFC 7591), sends the browser to the hosted login page, receives the authorization code on a loopback listener that checks `state`, redeems it with the PKCE verifier, and creates the profile's app password with the resulting access token. The flow is opt-in behind the hidden `--oauth` flag (`MZ_OAUTH`). `--oauth-client-id` (`MZ_OAUTH_CLIENT_ID`) uses a pre-registered client instead of registering one. The existing console-based browser login and `--no-browser` are unchanged. `mz-frontegg-client` gains `Authentication::AccessToken` for calling the admin API with an access token obtained elsewhere. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Make the OAuth authorization code flow the only browser login for `mz profile init`, and remove the console-based login along with its loopback handler and the `--oauth` flag. The OAuth endpoint is derived from `--cloud-endpoint` when `--admin-endpoint` is not given, as for the other admin API calls. `--no-browser` is unchanged. Co-Authored-By: Claude Opus 5.5 <[email protected]>
jubrad
force-pushed
the
justin/mz-oauth-pkce-login
branch
from
October 28, 2026 22:01
3251784 to
d036710
Compare
leedqin
approved these changes
Oct 28, 2026
jubrad
enabled auto-merge (squash)
October 28, 2026 22:19
maheshwarip
added a commit
that referenced
this pull request
Nov 6, 2026
Dates in RC snapshots are provisional until the release ships. One commit is added per snapshot; the final snapshot sets the real dates. ##⚠️ Needs the release owner before this can merge **1. This PR conflicts with `main`, and the conflict is substantive.** While this PR sat open, [#40201](#40201) ("docs: describe which SQL Server constraint Materialize records as a key") merged to `main` on 2026-11-05 and independently added its own `## v26.49.0` section — an `### Advisory {#v26.49-advisory}` plus an `### Improvements {#v26.49-improvements}` bullet. Both sides edit the same region of `_index.md`, so `mergeable` is `CONFLICTING`. Two problems ride along with the conflict: - **The same PR is described twice.** [#40201](#40201) documents the behavior of [#40131](#40131), which `rc.1` already published here as a Bug Fix ("Reduced the risk of new SQL Server source tables returning incorrect results after a unique constraint or primary key is dropped upstream…"). *Recommendation: keep `#40201`'s Advisory and Improvement and drop the `rc.1` bug-fix bullet* — the hand-written pair is more complete, carries the upgrade-impact guidance, and links `/ingest-data/sql-server/#unique-keys`. - **Anchor collision.** `main`'s `## v26.49.0` carries `{#v26.49-advisory}` and `{#v26.49-improvements}`; this branch's combined section carries `{#v26.49-improvements}`, `{#v26.49-agent-skills}`, and `{#v26.49-bug-fixes}`. Merging both defines `#v26.49-improvements` twice on one page. *Recommendation: rename this branch's three anchors to `{#v26.49.1-improvements}`, `{#v26.49.1-agent-skills}`, `{#v26.49.1-bug-fixes}`* — which is what the `v26.49.1` snapshot's review notes intended — *and leave `{#v26.49-advisory}` and `{#v26.49-improvements}` to `#40201`'s section.* `#40201`'s review links already point at `/releases/#v26.49-advisory`, and the compatibility YAML's `/releases/#v26491` target is unaffected. **2. Wording drift against the merged `v26.49.1` snapshot.** Commit `39e6b854a` landed here on 2026-11-05, before [MaterializeInc/mz-skills#355](MaterializeInc/mz-skills#355) merged on 2026-11-06. Two entries read differently on the branch than in the merged snapshot — the hydration improvement is "Investigate hydration on the cluster details page" here versus "See how a cluster hydrated, and why it did not" there, and the Freshness and Disk-column bullets differ likewise. Resolving the conflict above is the natural moment to decide which wording ships. ## Note on the section heading The `v26.49.1` patch train reused this branch (commit `39e6b854a`) rather than opening its own, so this PR now publishes a single combined `## v26.49.1` section — this train's `rc.1`–`rc.3` entries plus the three `v26.49.1` entries, with the Cloud version in parentheses. That is the house convention for a patch that ships in its parent's rollout week; compare the published `## v26.48.2` (`*Released to Materialize Cloud: 2026-10-29 (v26.48.0)*`). The compatibility YAML accordingly carries one `v26.49.1` row dated 2026-11-06, which is what actually shipped to Self-Managed that day. The `final` commit for this train is therefore **empty**: its increment carries no entries ([MaterializeInc/release#635](MaterializeInc/release#635) states "No PRs in `v26.49.0` require review", `v26.49.0-rc.3...v26.49.0` is the lone version-bump commit, and the agent-skills window is empty), and the real dates — Cloud **2026-11-05**, Self-Managed **2026-11-06** — were already set by `39e6b854a`. It exists so the branch's commit-subject ledger records that the snapshot was applied. ## Decisions required (carried over from `rc.1`) - **`DROP INDEX`** ([#39098](#39098)): it now rejects drops that used to succeed. Call it out as a breaking change rather than an Improvement? - **Console on large clusters** ([#40306](#40306)): the Cloud Console deploys separately, so this may already be live. Keep it in v26.49? - **Maintained Objects page** ([#40105](#40105)): omitted because it is behind a LaunchDarkly flag. Is it generally available? - **`mz profile init` OAuth** ([#40319](#40319)): omitted because the PR lists open items. Which `mz` release carries it? - **Upsert v2** ([#40189](#40189)): still omitted because `enable_upsert_v2` is `false`. Publish it? ## Snapshot drafts (with PR links) in mz-skills - rc.1: [release_notes.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/rc.1/release_notes.md), [omitted.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/rc.1/omitted.md), [MaterializeInc/mz-skills#351](MaterializeInc/mz-skills#351) - rc.2: [release_notes.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/rc.2/release_notes.md), [omitted.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/rc.2/omitted.md), [MaterializeInc/mz-skills#352](MaterializeInc/mz-skills#352) - rc.3: [release_notes.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/rc.3/release_notes.md), [omitted.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/rc.3/omitted.md), [MaterializeInc/mz-skills#353](MaterializeInc/mz-skills#353) - final: [release_notes.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/final/release_notes.md), [omitted.md](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/final/omitted.md) **Borderline PRs omitted:** rc.1: 10 — [rc.1/omitted.md#borderline](https://github.com/MaterializeInc/mz-skills/blob/main/data/mz-release-notes/v26.49.0/rc.1/omitted.md#borderline), rc.2: 0, rc.3: 0, final: 0 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Valerie Chiang <[email protected]> Co-authored-by: Pranshu Maheshwari <[email protected]>
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
mz profile initsigns in through a console page that creates an app password and hands it back tomz. This switches the browser login to the admin endpoint's OAuth 2.0 authorization server, using the standard flow for native apps: authorization code with PKCE (RFC 8252, RFC 7636). It works with any sign-in method the hosted login supports, including SSO and MFA, doesn't depend on a console page, and uses the same OAuth setup as the MCP server.Changes
Commit 1: add the OAuth login.
mz profile init(browser mode):127.0.0.1and registers a public OAuth client for that exact redirect URI through dynamic client registration (/oauth/dcr/register, RFC 7591)./oauth/authorizewith an S256 code challenge and a randomstate.state. Callbacks that don't match are rejected without ending the login./oauth/tokenwith the PKCE verifier.The hidden
--oauth-client-id(MZ_OAUTH_CLIENT_ID) uses a pre-registered client instead of registering one.mz-frontegg-clientgainsAuthentication::AccessTokenso the admin client can be used with a token obtained elsewhere.Commit 2: remove the console login. OAuth becomes the only browser login. The console-based handler and its loopback listener are removed. The OAuth endpoint is derived from
--cloud-endpointwhen--admin-endpointisn't given, matching the other admin API calls.--no-browseris unchanged.The console page that served the old flow stays for now, because released
mzversions and the VS Code extension still use it.Open items before this leaves draft
mzcurrently uses a random port. Eithermzmoves to a fixed loopback redirect that's added to the allowlist, or Frontegg needs to accept any port on loopback.mzmay be preferable.Tests
Adds
server::tests::test_oauth_callback_ignores_wrong_stateandserver::tests::test_oauth_callback_errors, which cover the callback listener'sstatecheck, user cancellation, and authorization server errors.Release notes
This release will change
mz profile initto sign in through Materialize's hosted login page using OAuth, which supports SSO and multi-factor authentication.🤖 Generated with Claude Code