Skip to content

mz: Use OAuth authorization code with PKCE for browser login - #40319

Merged
jubrad merged 2 commits into
MaterializeInc:mainfrom
jubrad:justin/mz-oauth-pkce-login
Oct 28, 2026
Merged

jubrad merged 2 commits into
MaterializeInc:mainfrom
jubrad:justin/mz-oauth-pkce-login

Conversation

@jubrad

@jubrad jubrad commented Oct 28, 2026 •

Copy link
Copy Markdown
Member

Motivation

mz profile init signs in through a console page that creates an app password and hands it back to mz. 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):

  1. Binds a loopback listener on 127.0.0.1 and registers a public OAuth client for that exact redirect URI through dynamic client registration (/oauth/dcr/register, RFC 7591).
  2. Opens /oauth/authorize with an S256 code challenge and a random state.
  3. Accepts the callback only when it carries the expected state. Callbacks that don't match are rejected without ending the login.
  4. Redeems the code at /oauth/token with the PKCE verifier.
  5. Creates the profile's app password through the admin API with the resulting access token, and stores the profile as before.

The hidden --oauth-client-id (MZ_OAUTH_CLIENT_ID) uses a pre-registered client instead of registering one. mz-frontegg-client gains Authentication::AccessToken so 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-endpoint when --admin-endpoint isn't given, matching the other admin API calls. --no-browser is unchanged.

The console page that served the old flow stays for now, because released mz versions and the VS Code extension still use it.

Open items before this leaves draft

  • Redirect URI allowlist. The workspace's hosted-login allowlist contains exact URLs, and mz currently uses a random port. Either mz moves to a fixed loopback redirect that's added to the allowlist, or Frontegg needs to accept any port on loopback.
  • Pre-registered client vs. per-login registration. Registering a client on every login leaves one registration behind per login. A single pre-registered client for mz may be preferable.
  • End-to-end verification against staging: whether the admin API accepts the OAuth access token, and which organization the app password lands in for users with several.

Tests

Adds server::tests::test_oauth_callback_ignores_wrong_state and server::tests::test_oauth_callback_errors, which cover the callback listener's state check, user cancellation, and authorization server errors.

Release notes

This release will change mz profile init to sign in through Materialize's hosted login page using OAuth, which supports SSO and multi-factor authentication.

🤖 Generated with Claude Code

@jubrad jubrad changed the title mz: Add OAuth authorization code login with PKCE mz: Use OAuth authorization code with PKCE for browser login Oct 28, 2026
@jubrad
jubrad marked this pull request as ready for review October 28, 2026 21:31
@jubrad
jubrad requested a review from a team as a code owner October 28, 2026 21:31
@jubrad
jubrad requested a review from a team as a code owner October 28, 2026 21:32
jubrad and others added 2 commits October 28, 2026 17:00
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
jubrad force-pushed the justin/mz-oauth-pkce-login branch from 3251784 to d036710 Compare October 28, 2026 22:01
@jubrad
jubrad enabled auto-merge (squash) October 28, 2026 22:19
@jubrad
jubrad merged commit 33dc314 into MaterializeInc:main Oct 28, 2026
80 checks passed
@bosconi bosconi mentioned this pull request Nov 4, 2026
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]>
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.

2 participants