-
Notifications
You must be signed in to change notification settings - Fork 495
SQL-64: Create OIDC authenticator #34690
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SQL-64: Create OIDC authenticator #34690
Conversation
ffdddbc to
63f323e
Compare
bb5861a to
90002f0
Compare
b59884a to
8eb54d2
Compare
src/authenticator/src/oidc.rs
Outdated
| async fn expired(&mut self) { | ||
| // This session never expires - wait forever | ||
| // TODO (SangJunBak): Implement expiration. | ||
| std::future::pending().await |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
src/authenticator/src/oidc.rs
Outdated
| // TODO (SangJunBak): Add a configuration variable for the JWKS set and | ||
| // a boolean jwksFetchFromIssuer. | ||
| let jwks_uri = issuer_url | ||
| .join(".well-known/jwks.json") |
There was a problem hiding this comment.
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
src/authenticator/src/oidc.rs
Outdated
| let mut validation = Validation::new(header.alg); | ||
| validation.set_issuer(&[&self.issuer]); | ||
| // TODO (SangJunBak): Validate audience based on configuration. | ||
| validation.validate_aud = false; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
src/authenticator/Cargo.toml
Outdated
| async-trait = "0.1" | ||
| jsonwebtoken = "9.3.1" | ||
| mz-adapter = { path = "../adapter", default-features = false } | ||
| mz-authenticator-types = { path = "../authenticator-types" } |
There was a problem hiding this comment.
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.
src/environmentd/src/http.rs
Outdated
| Authenticator::Password(adapter_client) => match creds { | ||
| Some(Credentials::Password { username, password }) => { | ||
| let auth_response = adapter_client | ||
| adapter_client |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adapter_client.authenticate
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sgtm!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be addressed!
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
ef75f70 to
b59f394
Compare
|
@alex-hunt-materialize Feedback should all be addressed! |
8462470 to
6794a1a
Compare
teskje
left a comment
There was a problem hiding this 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.
src/authenticator/src/oidc.rs
Outdated
| Err(OidcError::NoMatchingKey) | ||
| } | ||
|
|
||
| pub async fn validate_access_token( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
validate_token sgtm!
teskje
left a comment
There was a problem hiding this 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.
6794a1a to
2cf9eb5
Compare
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
316bc17 to
db39761
Compare
- Change OidcDecodingKey debug struct name to reflect struct - Rename validate_access_token to validate_token - Don't namespace most jsonwebtoken imports
db39761 to
f842f38
Compare
teskje
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
See commit messages for details.
Decided against using a system parameter for now to gate this feature, mainly because:
OidcMotivation
For an overview of what still needs to be implemented, a summary can be found on Linear (link)
To try:
demo.mov
Tips for reviewer
Some open questions I had regarding my implementation:
internal_user_metadatachange, it felt cleaner to combine it withrole_metadatainsession.rs. However, I decided against it since it would move the source of truth of the superuser status from theSession > SessionVars > UsertoSession, something adapter: move user field from Session to SessionVars #17961 was trying to avoid. Furthermore,role_metadataholds information regarding not only its role, but other roles too.Checklist
$T ⇔ Proto$Tmapping (possibly in a backwards-incompatible way), then it is tagged with aT-protolabel.