-
Notifications
You must be signed in to change notification settings - Fork 918
GODRIVER-2911: Add machine flow OIDC authentication #1678
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
Conversation
…not go live, I'm sure
…r OIDC that is probably, maybe, possibly correct
… need to get down to Handshake instead of creating the Authenticator in Handshake as we do now
…o I'm sure it's right
API Change Report./cmd/testoidcauthcompatible changespackage added ./mongo/optionscompatible changesCredential.OIDCHumanCallback: added ./x/mongo/driverincompatible changesConnection.OIDCTokenGenID: added compatible changesAuthConfig: added ./x/mongo/driver/authincompatible changes##(*DefaultAuthenticator).Auth: changed from func(context.Context, *Config) error to func(context.Context, *./x/mongo/driver.AuthConfig) error compatible changes(*DefaultAuthenticator).Reauth: added ./x/mongo/driver/drivertestcompatible changes(*ChannelConn).OIDCTokenGenID: added ./x/mongo/driver/operationcompatible changes(*AbortTransaction).Authenticator: added ./x/mongo/driver/sessionincompatible changesLoadBalancedTransactionConnection.OIDCTokenGenID: added ./x/mongo/driver/topologycompatible changes(*Connection).OIDCTokenGenID: added |
| import ( | ||
| "go.mongodb.org/mongo-driver/x/mongo/driver" | ||
| ) | ||
|
|
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.
Redeclarations like this are just to avoid breaking user code.
cmd/testoidcauth/main.go
Outdated
| } | ||
|
|
||
| // Poison the cache with a random token | ||
| client.GetAuthenticator().(*auth.OIDCAuthenticator).SetAccessToken("some random happy sunshine string") |
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 should try to avoid adding public methods if possible, especially if it's only used for test support. Since this is only used for testing, I recommend using some reflection/unsafe code to extract the authenticator rather than exposing it via GetAuthenticator.
E.g.
{
clientElem := reflect.ValueOf(client).Elem()
authenticatorField := clientElem.FieldByName("authenticator")
authenticatorField = reflect.NewAt(
authenticatorField.Type(),
unsafe.Pointer(authenticatorField.UnsafeAddr())).Elem()
authenticatorField.Interface().(*auth.OIDCAuthenticator).SetAccessToken("some random happy sunshine string")
}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 didn't know you could do this, thanks, much preferable
x/mongo/driver/operation/insert.go
Outdated
|
|
||
| // InsertResult represents an insert result returned by the server. | ||
| type InsertResult struct { | ||
| authenticator driver.Authenticator |
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 there a reason to add authenticator to InsertResult?
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 added these with a python script and missed this one :)
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.
Looks like I added a few other extra ones, should all be gone now
x/mongo/driver/driver.go
Outdated
| // OIDCArgs contains the arguments for the OIDC callback. | ||
| type OIDCArgs struct { | ||
| Version int | ||
| Timeout time.Time |
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 seems like Timeout is used to communicate a deadline to the OIDC callback, which is redundant with the context.Context parameter that's part of the OIDCCallback function signature. Is there any other reason to keep Timeout?
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.
Nope, I think you're right
| ClientID string `bson:"clientId"` | ||
| RequestScopes []string `bson:"requestScopes"` | ||
| } | ||
|
|
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 most often see "upgrading" Go interfaces used as a backward compatibility mechanism. IMO some extra no-op functions is better than runtime type assertions. I agree with the current approach.
x/mongo/driver/auth/oidc.go
Outdated
| oa.mu.Lock() | ||
| defer oa.mu.Unlock() | ||
| var err error | ||
|
|
||
| if cfg == nil { | ||
| return newAuthError(fmt.Sprintf("config must be set for %q authentication", MongoDBOIDC), nil) | ||
| } | ||
| oa.cfg = cfg | ||
|
|
||
| if oa.accessToken != "" { | ||
| err = ConductSaslConversation(ctx, cfg, "$external", &oidcOneStep{ |
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.
Holding an exclusive lock while we run the SASL conversation will limit the auth part of connection establishment to one-at-a-time, overriding the intended behavior of maxConnecting. Instead, we should only hold a lock when invalidating and acquiring a new access token.
Unfortunately that means the strategy of storing the auth.Config on the OIDCAuthenticator also won't work.
E.g. changes to prevent locking during the SASL conversation and allow multiple Config instances:
func (oa *OIDCAuthenticator) invalidateAccessToken(tokenGenID uint64) {
oa.mu.Lock()
defer oa.mu.Unlock()
if oa.tokenGenID <= tokenGenID {
oa.accessToken = ""
}
// ...
}
func (oa *OIDCAuthenticator) getAccessToken(...) (string, uint64, error) {
oa.mu.Lock()
defer oa.mu.Unlock()
if oa.accessToken != "" {
return oa.accessToken, oa.tokenGenID, nil
}
// ...
}
func (oa *OIDCAuthenticator) Auth(ctx context.Context, cfg *Config) error {
var err error
if cfg == nil {
return newAuthError(fmt.Sprintf("config must be set for %q authentication", MongoDBOIDC), nil)
}
accessToken, tokenGenID, err := oa.getAccessToken(...)
if oa.accessToken != "" {
err = ConductSaslConversation(ctx, cfg, "$external", &oidcOneStep{
userName: oa.userName,
accessToken: oa.accessToken,
})
if err == nil {
return nil
}
oa.invalidateAccessToken(tokenGenID)
time.Sleep(invalidateSleepTimeout)
}
oa.mu.Lock()
defer oa.mu.Unlock()
// ...
}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 think this works, let me run the tests. My concern is machine_1_2, but I do think this works.
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 worked
x/mongo/driver/auth/oidc.go
Outdated
|
|
||
| // Reauth reauthenticates the connection when the server returns a 391 code. Reauth is part of the | ||
| // driver.Authenticator interface. | ||
| func (oa *OIDCAuthenticator) Reauth(ctx context.Context) error { |
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.
Reauth needs to take accept an AuthConfig that has all the inputs for authenticating a specific connection, not the connection most recently configured in oa.cfg.
E.g.
func (oa *OIDCAuthenticator) Reauth(
ctx context.Context,
cfg *AuthConfig,
) error {It's not clear exactly how to get all the info for an AuthConfig in Operation.Execute.
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 don't see how we can make that work. The python and rust driver both store the necessary information in the Authenticator :/
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.
Seems we found a good way to make this work
Co-authored-by: Matt Dale <[email protected]>
matthewdale
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.
Some small improvement suggestions, but the overall behavior looks good!
x/mongo/driver/auth/oidc.go
Outdated
| return runSaslConversation(ctx, | ||
| cfg, | ||
| newSaslConversation(&oidcOneStep{accessToken: accessToken}, "$external", 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.
Can this use ConductSaslConversation instead? That would allow merging runSaslConversation and ConductSaslConversation together.
E.g.
return ConductSaslConversation(
ctx,
cfg,
"$external",
&oidcOneStep{accessToken: accessToken})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.
Yea, this was a holdover from something I was testing, and I didn't think anyone would mind. (runSaslConversation didn't exist before)
x/mongo/driver/auth/oidc.go
Outdated
| func (oa *OIDCAuthenticator) doAuthHuman(_ context.Context, _ *Config, _ OIDCCallback) error { | ||
| // TODO GODRIVER-3246: Implement OIDC human flow | ||
| // Println is for linter | ||
| fmt.Println("OIDC human flow not implemented yet", oa.idpInfo) |
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.
Remove print statement.
| fmt.Println("OIDC human flow not implemented yet", oa.idpInfo) |
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.
The linter fails if I remove the print ;) Note this will be removed in the next ticket
Oh, I could just return an error that uses idpInfo... yes
| subCtx, cancel := context.WithTimeout(ctx, machineCallbackTimeout) | ||
| accessToken, err := oa.getAccessToken(subCtx, |
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.
The line in the OIDC spec that this timeout comes from is ambiguous with respect to the Go Driver:
If CSOT is not applied, then the driver MUST use 1 minute as the timeout.
All Go Driver blocking APIs support timeouts, so from that perspective, CSOT is always applied. However, the Go Driver also includes the concept of CSOT being "enabled" or "not enabled", which was required to retrofit the CSOT behavior into the existing driver that already supported timeouts (for example, see usage of csot.IsTimeoutContext). From that perspective, we should only apply the 1-minute timeout when CSOT is "not enabled".
As implemented, the code always applies the 1-minute timeout, overriding any longer timeouts. That seems unlikely to break anything because waiting 1 minute for an access token will probably cause other problems (e.g. the default connectTimeout is 30 seconds).
Overall, I think this code is safe, but we should add a comment describing the interpretation of the OIDC spec we're using here.
E.g. comment:
The CSOT spec says to apply a 1-minute timeout if "CSOT is not applied". That's ambiguous for the v1.x Go Driver because it could mean either "no timeout provided" or "CSOT not enabled". Always use a maximum timeout duration of 1 minute, allowing us to ignore the ambiguity. Contexts with a shorter timeout are unaffected.
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 exactly the sort of context I am missing, thank you!
…in favor of using oa.idpInfo in error message
mongo/options/clientoptions.go
Outdated
| OIDCMachineCallback driver.OIDCCallback | ||
| OIDCHumanCallback driver.OIDCCallback |
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 to canonically define or re-define the OIDCCallback type and associated structs here so the public API doesn't include symbols from the experimental API.
E.g. types that need to be defined in the options package:
// OIDCCallback is the type for both Human and Machine Callback flows. RefreshToken will always be
// nil in the OIDCArgs for the Machine flow.
type OIDCCallback func(context.Context, *OIDCArgs) (*OIDCCredential, error)
// OIDCArgs contains the arguments for the OIDC callback.
type OIDCArgs struct {
Version int
IDPInfo *IDPInfo
RefreshToken *string
}
// OIDCCredential contains the access token and refresh token.
type OIDCCredential struct {
AccessToken string
ExpiresAt *time.Time
RefreshToken *string
}It may be possible to canonically define the OIDCCallback, OIDCArgs, and OIDCCredential types in the options package, and use those types in the driver and auth` packages. If not, we will have to add logic to convert between the 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.
Tell me if this solution works for you, I think it's good.
…experimental options package
matthewdale
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.
Looks good! 👍
GODRIVER-2911
Summary
This is a full implementation of machine flow OIDC with token invalidation and reauthentication. It is missing configuration enforcement, which is split into GODRIVER-3249 to keep the PR smaller.
This currently does not have the tests, but I wanted to give an opportunity for architecture feedback. This should work since it's following the same algorithm as the rust code, but there could be something I'm missing here with, for instance, how reauthentication should work since this is different than how the rust driver does auth, and I want to make sure this looks correct.
Biggest changes
Commands andOperations to handle reauthConnectioninterface (:()Background & Motivation
epic: https://jira.mongodb.org/browse/GODRIVER-2574