Skip to content

Conversation

@SangJunBak
Copy link
Contributor

@SangJunBak SangJunBak commented Jan 12, 2026

See commit messages for details.

Decided against using a system parameter for now to gate this feature, mainly because:

  • Feature's already gated by the user having to set their authenticatorkind as Oidc
  • The rigging of system parameters to authentication code is a bit unclear still
  • We're going to rig up OIDC config to system parameters eventually as part of a later phase

Motivation

For an overview of what still needs to be implemented, a summary can be found on Linear (link)

To try:

  1. Follow the quickstart guide to get an authentication server (Ory) running in Docker https://www.ory.com/docs/hydra/self-hosted/quickstart
  2. Once running in docker, run:
bin/environmentd \
-- --oidc-issuer="http://127.0.0.1:4444" \
--listeners-config-path='src/materialized/ci/listener_configs/oidc.json'
  1. In the cloned repo, run the following commands to create a client and get the access token by following the authorization code flow
code_client=$(docker compose -f quickstart.yml exec hydra \
    hydra create client \
    --endpoint http://127.0.0.1:4445 \
    --grant-type authorization_code,refresh_token \
    --response-type code,id_token \
    --format json \
    --scope openid --scope offline --scope profile --scope email\
    --access-token-strategy jwt \
    --redirect-uri http://127.0.0.1:5555/callback)

code_client_id=$(echo $code_client | jq -r '.client_id')
code_client_secret=$(echo $code_client | jq -r '.client_secret')

docker compose -f quickstart.yml exec hydra \
    hydra perform authorization-code \
    --client-id $code_client_id \
    --client-secret $code_client_secret \
    --endpoint http://127.0.0.1:4444/ \
    --port 5555 \
    --scope openid --scope offline --scope profile --scope email
  1. Run `PGPASSWORD="eyJ..." psql -h localhost -p 6875 -U [email protected] materialize
demo.mov

Tips for reviewer

Some open questions I had regarding my implementation:

  • Not sure if GenericOidcAuthenticator is the right word for the authenticator. Open to feedback!
  • For the internal_user_metadata change, it felt cleaner to combine it with role_metadata in session.rs. However, I decided against it since it would move the source of truth of the superuser status from the Session > SessionVars > User to Session, something adapter: move user field from Session to SessionVars #17961 was trying to avoid. Furthermore, role_metadata holds information regarding not only its role, but other roles too.

Checklist

  • This PR has adequate test coverage / QA involvement has been duly considered. (trigger-ci for additional test/nightly runs)
  • This PR has an associated up-to-date design doc, is a design doc (template), or is sufficiently small to not require a design.
  • If this PR evolves an existing $T ⇔ Proto$T mapping (possibly in a backwards-incompatible way), then it is tagged with a T-proto label.
  • If this PR will require changes to cloud orchestration or tests, there is a companion cloud PR to account for those changes that is tagged with the release-blocker label (example).
  • If this PR includes major user-facing behavior changes, I have pinged the relevant PM to schedule a changelog post.

@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch 5 times, most recently from ffdddbc to 63f323e Compare January 14, 2026 17:10
@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch 8 times, most recently from bb5861a to 90002f0 Compare January 21, 2026 06:34
@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch 2 times, most recently from b59884a to 8eb54d2 Compare January 21, 2026 08:15
@SangJunBak SangJunBak marked this pull request as ready for review January 21, 2026 08:17
@SangJunBak SangJunBak requested review from a team and ggevay as code owners January 21, 2026 08:17
@SangJunBak SangJunBak removed the request for review from ggevay January 21, 2026 08:17
async fn expired(&mut self) {
// This session never expires - wait forever
// TODO (SangJunBak): Implement expiration.
std::future::pending().await
Copy link
Contributor

Choose a reason for hiding this comment

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

We need expiration support, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes! Same motivation for this TODO here #34690 (comment). I figured I'd batch it with the refresh token work

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have tickets for the follow up work? I just don't want us to forget this stuff.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not github issues but I have them recorded in Linear as tasks/subtasks https://linear.app/materializeinc/project/oidc-for-self-managed-3f1f7c14cfb5/issues

// TODO (SangJunBak): Add a configuration variable for the JWKS set and
// a boolean jwksFetchFromIssuer.
let jwks_uri = issuer_url
.join(".well-known/jwks.json")
Copy link
Contributor

Choose a reason for hiding this comment

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

This path, while commonly used, is not part of the OIDC specification. However, .well-known/openid-configuration IS actually part of the specification, and contains a required jwks_uri field. We should use the URI from that field instead.

https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata

let mut validation = Validation::new(header.alg);
validation.set_issuer(&[&self.issuer]);
// TODO (SangJunBak): Validate audience based on configuration.
validation.validate_aud = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

If it isn't configurable, we should default to validating the audience.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added audience as an input here b59f394!

async-trait = "0.1"
jsonwebtoken = "9.3.1"
mz-adapter = { path = "../adapter", default-features = false }
mz-authenticator-types = { path = "../authenticator-types" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Please always set default-features = false for all mz crates. This is needed to avoid pulling in workspace-hack into cloud.

Authenticator::Password(adapter_client) => match creds {
Some(Credentials::Password { username, password }) => {
let auth_response = adapter_client
adapter_client
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure how much it matters, but there was some safety in us using the response from the authenticate call. Now that it isn't being used, there isn't anything preventing us from accidentally not calling it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah do you have more context on this? i.e. who was involved.

there isn't anything preventing us from accidentally not calling it.

What is the "it" you're referring to?

Copy link
Contributor

Choose a reason for hiding this comment

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

adapter_client.authenticate

Copy link
Member

Choose a reason for hiding this comment

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

Ah yeah, there is a risk there. @SangJunBak I think alexs point is well taken, though not urgently something you need to change. His point is that before when we had to return data from the auth response, it meant we couldn't forget to call adapter_client.authenticate. You could have authenticate return some sentinel type that we need to return or we could just be very careful.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'd really like a sentinel type added, so we can't forget to call it. It's way to easy to get this wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sgtm!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should be addressed!

Copy link
Member

Choose a reason for hiding this comment

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

Yeah this is great, thank you.

// TODO (SangJunBak): Use oidc_auth_enabled boolean and Authenticator::OIDC
// to decide whether or not we want to use OIDC authentication or password
// authentication.
let (_oidc_auth_enabled, options) = extract_oidc_auth_enabled_from_options(options);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this just a placeholder for later? Changing this behavior later is a breaking change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a placeholder for later! More specifically, I don't plan on releasing this to customers in its current state. I wanted to get eyes on it sooner than later and I figured we're effectively gating this feature until the orchestratord changes.

@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch 6 times, most recently from ef75f70 to b59f394 Compare January 22, 2026 01:35
@SangJunBak
Copy link
Contributor Author

@alex-hunt-materialize Feedback should all be addressed!

@aljoscha aljoscha requested review from aljoscha and teskje January 28, 2026 09:21
@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch from 8462470 to 6794a1a Compare January 28, 2026 14:53
@SangJunBak SangJunBak requested a review from teskje January 28, 2026 15:42
Copy link
Contributor

@teskje teskje left a comment

Choose a reason for hiding this comment

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

Not through yet, releasing a first batch of comments.

Err(OidcError::NoMatchingKey)
}

pub async fn validate_access_token(
Copy link
Contributor

Choose a reason for hiding this comment

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

This is for validating ID tokens, not access tokens, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah it technically can be both, but ideally ID tokens!

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I didn't realize access tokens are JWTs as well. They wouldn't contain OidcClaims though, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah according to the spec, they don't need to be a JWT. And the answer is they shouldn't, but they could. i.e. there's no reason why they couldn't create a custom claim like "materialize_username", then use that claim to identify them instead of email or sub (this will be implemented in a future PR). This is similar to how we treat access tokens as ID tokens in Frontegg.

But I think spiritually, this would be equivalent to an ID token. Maybe we should just rename this to validate_token?

Copy link
Contributor

Choose a reason for hiding this comment

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

validate_token sgtm!

Copy link
Contributor

@teskje teskje left a comment

Choose a reason for hiding this comment

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

Reviewed everything!

Got a bunch of comments. My main concern is about making the audience validation optional. That seems like a potential security vulnerability and if it is we should disallow it.

Otherwise I've been thinking that calling this authentication method "OIDC" is a bit misleading, since we are not doing the full OIDC authorization flow. We only do the OIDC ID token validation part. But naming things is hard, and I can't come up with a better name either.

@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch from 6794a1a to 2cf9eb5 Compare February 9, 2026 16:09
@SangJunBak SangJunBak requested a review from teskje February 9, 2026 16:17
We extract
- authenticate
- validate_access_token

Two methods used to authenticate HTTP and pgwire sessions out of the Frontegg authenticator. The goal is we can reuse these methods for a generic OIDC authenticator, used for self managed SSO
- Create an OIDC authenticator kind and a minimal set of config variables using CLI args
- Implement JWK fetch on validate and also cache by the JWK key id.
I noticed that we were doing this weird round trip of getting internal user metadata from the catalog during authentication, then passing it back when initializing the session. By just doing this on startup, we:
- Remove extraneous code
- Open up ease of creating a unified interface for OIDC clients
- Introduced a new `mz-oidc-mock` package
- Implemented tests for OIDC authentication
- Add functionality to extract `oidc_auth_enabled` from startup options, allowing us to use the password authenticator in the future
Before we were mistakenly fetching from jwks.json instead of getting it from the configuration endpoint
…in authentication

- Updated comments to use backticks for OIDC issuer URL
- Changed password handling in oidc http/ws auth to include the username when validating access tokens.
- Simplified OIDC mock server structure by removing the base URL field
- Remove unneeded assertion on role existence
- Add aud claim
- Introduced `oidc_audience` field in OidcConfig to validate JWT's `aud` claim. This follows the spec in the design doc
- Added OIDC mock server audience claims support
- Added tests for audience validation and when we don't need to
Didn't realize we already had a shared crate!
Due to changed restrictions after a discussion with an external advisor, we decided we no longer need to implement the refresh token flow. However, this also means we no longer have the need for a shared OidcAuthenticator trait.
Before when we had to return internal user metadata data from the auth response, it meant we couldn't forget to call adapter_client.authenticate. By introducing a sentinel type, we make it harder for a developer to. 

We also combine `validate_access_token` into `authenticate` for GenericOidcAuthenticator.
Annotate TODOs with project
@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch from 316bc17 to db39761 Compare February 9, 2026 17:00
- Change OidcDecodingKey debug struct name to reflect struct
- Rename validate_access_token to validate_token
- Don't namespace most jsonwebtoken imports
@SangJunBak SangJunBak force-pushed the jun/add-system-parameters-for-oidc branch from db39761 to f842f38 Compare February 9, 2026 18:23
Copy link
Contributor

@teskje teskje left a comment

Choose a reason for hiding this comment

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

LGTM!

@SangJunBak SangJunBak merged commit c61ef02 into MaterializeInc:main Feb 9, 2026
133 checks passed
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.

4 participants