diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 21f1bf663f..8d52a677c7 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1963,11 +1963,11 @@ tasks: popd ./.evergreen/run-deployed-lambda-aws-tests.sh - - name: "oidc-auth-test-latest" + - name: "oidc-auth-test" commands: - func: "run-oidc-auth-test-with-test-credentials" - - name: "oidc-auth-test-azure-latest" + - name: "oidc-auth-test-azure" commands: - command: shell.exec params: @@ -1992,7 +1992,7 @@ tasks: export AZUREOIDC_TEST_CMD="PROJECT_DIRECTORY='.' OIDC_ENV=azure OIDC=oidc ./etc/run-oidc-test.sh ./test" bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh - - name: "oidc-auth-test-gcp-latest" + - name: "oidc-auth-test-gcp" commands: - command: shell.exec params: @@ -2306,13 +2306,14 @@ task_groups: - func: fix-absolute-paths - func: make-files-executable - func: assume-ec2-role - - command: shell.exec + - command: subprocess.exec params: - shell: bash + binary: bash include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] - script: | - ${PREPARE_SHELL} - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh + env: + MONGODB_VERSION: "8.0" + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh teardown_task: - command: subprocess.exec params: @@ -2322,7 +2323,7 @@ task_groups: setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - - oidc-auth-test-latest + - oidc-auth-test - name: testazureoidc_task_group setup_group: @@ -2346,7 +2347,7 @@ task_groups: setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - - oidc-auth-test-azure-latest + - oidc-auth-test-azure - name: testgcpoidc_task_group setup_group: @@ -2370,7 +2371,7 @@ task_groups: setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - - oidc-auth-test-gcp-latest + - oidc-auth-test-gcp - name: test-aws-lambda-task-group setup_group: diff --git a/cmd/testoidcauth/main.go b/cmd/testoidcauth/main.go index a3c1db5d77..aede5cbbfe 100644 --- a/cmd/testoidcauth/main.go +++ b/cmd/testoidcauth/main.go @@ -18,6 +18,7 @@ import ( "unsafe" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/event" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/x/mongo/driver/auth" @@ -38,7 +39,7 @@ func tokenFile(user string) string { return path.Join(oidcTokenDir, user) } -func connectAdminClinet() (*mongo.Client, error) { +func connectAdminClient() (*mongo.Client, error) { return mongo.Connect(context.Background(), options.Client().ApplyURI(uriAdmin)) } @@ -56,6 +57,25 @@ func connectWithHumanCB(uri string, cb options.OIDCCallback) (*mongo.Client, err return mongo.Connect(context.Background(), opts) } +func connectWithHumanCBAndUser(uri string, principal string, cb options.OIDCCallback) (*mongo.Client, error) { + opts := options.Client().ApplyURI(uri) + switch principal { + case "test_user1", "test_user2": + opts.Auth.Username = explicitUser(principal) + default: + opts.Auth.Username = principal + } + opts.Auth.OIDCHumanCallback = cb + return mongo.Connect(context.Background(), opts) +} + +func connectWithHumanCBAndMonitor(uri string, cb options.OIDCCallback, m *event.CommandMonitor) (*mongo.Client, error) { + opts := options.Client().ApplyURI(uri) + opts.Monitor = m + opts.Auth.OIDCHumanCallback = cb + return mongo.Connect(context.Background(), opts) +} + func connectWithMachineCBAndProperties(uri string, cb options.OIDCCallback, props map[string]string) (*mongo.Client, error) { opts := options.Client().ApplyURI(uri) @@ -101,6 +121,7 @@ func main() { aux("human_1_5_multiplPrincipalNoUser", human15mulitplePrincipalNoUser) aux("human_1_6_allowedHostsBlocked", human16allowedHostsBlocked) aux("human_1_7_allowedHostsInConnectionStringIgnored", human17AllowedHostsInConnectionStringIgnored) + aux("human_1_8_machineIDPHumanCallback", human18MachineIDPHumanCallback) aux("human_2_1_validCallbackInputs", human21validCallbackInputs) aux("human_2_2_CallbackReturnsMissingData", human22CallbackReturnsMissingData) aux("human_2_3_RefreshTokenIsPassedToCallback", human23RefreshTokenIsPassedToCallback) @@ -111,6 +132,7 @@ func main() { aux("human_4_3_reauthenticationSucceedsAfterRefreshFails", human43ReauthenticationSucceedsAfterRefreshFails) aux("human_4_4_reauthenticationFails", human44ReauthenticationFails) case "azure": + aux("machine_2_5_InvalidUseofAllowedHosts", machine25InvalidUseofAllowedHosts) aux("machine_5_1_azureWithNoUsername", machine51azureWithNoUsername) aux("machine_5_2_azureWithNoUsername", machine52azureWithBadUsername) case "gcp": @@ -331,6 +353,26 @@ func machine24invalidClientConfigurationWithCallback() error { return nil } +func machine25InvalidUseofAllowedHosts() error { + _, err := connectWithMachineCBAndProperties(uriSingle, func(ctx context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) { + t := time.Now().Add(time.Hour) + return &options.OIDCCredential{ + AccessToken: "", + ExpiresAt: &t, + RefreshToken: nil, + }, nil + }, + map[string]string{ + "ENVIRONMENT": "azure", + "ALLOWED_HOSTS": "", + }, + ) + if err == nil { + return fmt.Errorf("machine_2_5: succeeded building client when it should fail") + } + return nil +} + func machine31failureWithCachedTokensFetchANewTokenAndRetryAuth() error { callbackCount := 0 var callbackFailed error @@ -424,7 +466,7 @@ func machine33UnexpectedErrorCodeDoesNotClearTheCache() error { var callbackFailed error countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("machine_3_3: failed connecting admin client: %v", err) } @@ -498,7 +540,7 @@ func machine41ReauthenticationSucceeds() error { var callbackFailed error countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("machine_4_1: failed connecting admin client: %v", err) } @@ -563,7 +605,7 @@ func machine42ReadCommandsFailIfReauthenticationFails() error { firstCall := true countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("machine_4_2: failed connecting admin client: %v", err) } @@ -643,7 +685,7 @@ func machine43WriteCommandsFailIfReauthenticationFails() error { firstCall := true countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("machine_4_3: failed connecting admin client: %v", err) } @@ -763,8 +805,7 @@ func human12singlePrincipalExplicitUsername() error { var callbackFailed error countMutex := sync.Mutex{} - opts := options.Client().ApplyURI(uriSingle) - opts.Auth.OIDCHumanCallback = func(ctx context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) { + client, err := connectWithHumanCBAndUser(uriSingle, "test_user1", func(ctx context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) { countMutex.Lock() defer countMutex.Unlock() callbackCount++ @@ -779,9 +820,7 @@ func human12singlePrincipalExplicitUsername() error { ExpiresAt: &t, RefreshToken: nil, }, nil - } - opts.Auth.Username = explicitUser("test_user1") - client, err := mongo.Connect(context.Background(), opts) + }) if err != nil { return fmt.Errorf("human_1_2: failed connecting client: %v", err) } @@ -1000,6 +1039,53 @@ func human17AllowedHostsInConnectionStringIgnored() error { return nil } +func human18MachineIDPHumanCallback() error { + if _, ok := os.LookupEnv("OIDC_IS_LOCAL"); !ok { + return nil + } + callbackCount := 0 + + var callbackFailed error + countMutex := sync.Mutex{} + + client, err := connectWithHumanCBAndUser(uriSingle, "test_machine", func(ctx context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) { + countMutex.Lock() + defer countMutex.Unlock() + callbackCount++ + t := time.Now().Add(time.Hour) + tokenFile := tokenFile("test_machine") + accessToken, err := os.ReadFile(tokenFile) + if err != nil { + callbackFailed = fmt.Errorf("human_1_8: failed reading token file: %v", err) + } + return &options.OIDCCredential{ + AccessToken: string(accessToken), + ExpiresAt: &t, + RefreshToken: nil, + }, nil + }) + + defer client.Disconnect(context.Background()) + + if err != nil { + return fmt.Errorf("human_1_8: failed connecting client: %v", err) + } + + coll := client.Database("test").Collection("test") + + _, err = coll.Find(context.Background(), bson.D{}) + if err != nil { + return fmt.Errorf("human_1_8: failed executing Find: %v", err) + } + countMutex.Lock() + defer countMutex.Unlock() + if callbackCount != 1 { + return fmt.Errorf("human_1_8: expected callback count to be 1, got %d", callbackCount) + } + return callbackFailed + +} + func human21validCallbackInputs() error { callbackCount := 0 var callbackFailed error @@ -1084,7 +1170,7 @@ func human23RefreshTokenIsPassedToCallback() error { var callbackFailed error countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("human_2_3: failed connecting admin client: %v", err) } @@ -1152,7 +1238,7 @@ func human23RefreshTokenIsPassedToCallback() error { } func human31usesSpeculativeAuth() error { - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("human_3_1: failed connecting admin client: %v", err) } @@ -1213,7 +1299,7 @@ func human31usesSpeculativeAuth() error { func human32doesNotUseSpecualtiveAuth() error { var callbackFailed error - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("human_3_2: failed connecting admin client: %v", err) } @@ -1266,7 +1352,134 @@ func human32doesNotUseSpecualtiveAuth() error { } func human41ReauthenticationSucceeds() error { - return nil + callbackCount := 0 + var callbackFailed error + countMutex := sync.Mutex{} + + adminClient, err := connectAdminClient() + if err != nil { + return fmt.Errorf("human_4_1: failed connecting admin client: %v", err) + } + defer adminClient.Disconnect(context.Background()) + + clearChannels := func(s chan *event.CommandStartedEvent, succ chan *event.CommandSucceededEvent, f chan *event.CommandFailedEvent) { + for len(s) > 0 { + <-s + } + for len(succ) > 0 { + <-succ + } + for len(f) > 0 { + <-f + } + } + + started := make(chan *event.CommandStartedEvent, 100) + succeeded := make(chan *event.CommandSucceededEvent, 100) + failed := make(chan *event.CommandFailedEvent, 100) + + monitor := event.CommandMonitor{ + Started: func(ctx context.Context, e *event.CommandStartedEvent) { + started <- e + }, + Succeeded: func(ctx context.Context, e *event.CommandSucceededEvent) { + succeeded <- e + }, + Failed: func(ctx context.Context, e *event.CommandFailedEvent) { + failed <- e + }, + } + + client, err := connectWithHumanCBAndMonitor(uriSingle, func(ctx context.Context, args *options.OIDCArgs) (*options.OIDCCredential, error) { + countMutex.Lock() + defer countMutex.Unlock() + callbackCount++ + t := time.Now().Add(time.Hour) + tokenFile := tokenFile("test_user1") + accessToken, err := os.ReadFile(tokenFile) + if err != nil { + callbackFailed = fmt.Errorf("human_4_1: failed reading token file: %v", err) + } + return &options.OIDCCredential{ + AccessToken: string(accessToken), + ExpiresAt: &t, + RefreshToken: nil, + }, nil + }, &monitor) + if err != nil { + return fmt.Errorf("human_4_1: failed connecting client: %v", err) + } + defer client.Disconnect(context.Background()) + clearChannels(started, succeeded, failed) + + coll := client.Database("test").Collection("test") + _, err = coll.Find(context.Background(), bson.D{}) + if err != nil { + return fmt.Errorf("human_4_1: Find failed when it should succeed") + } + countMutex.Lock() + if callbackCount != 1 { + return fmt.Errorf("human_4_1: expected callback count to be 1, got %d", callbackCount) + } + countMutex.Unlock() + clearChannels(started, succeeded, failed) + + res := adminClient.Database("admin").RunCommand(context.Background(), bson.D{ + {Key: "configureFailPoint", Value: "failCommand"}, + {Key: "mode", Value: bson.D{ + {Key: "times", Value: 1}, + }}, + {Key: "data", Value: bson.D{ + {Key: "failCommands", Value: bson.A{ + "find", + }}, + {Key: "errorCode", Value: 391}, + }}, + }) + + if res.Err() != nil { + return fmt.Errorf("machine_4_1: failed setting failpoint: %v", res.Err()) + } + + _, err = coll.Find(context.Background(), bson.D{}) + if err != nil { + return fmt.Errorf("human_4_1: Second find failed when it should succeed") + } + countMutex.Lock() + if callbackCount != 2 { + return fmt.Errorf("human_4_1: expected callback count to be 2, got %d", callbackCount) + } + countMutex.Unlock() + + if len(started) != 2 { + return fmt.Errorf("human_4_1: expected 2 finds started, found %d", len(started)) + } + for len(started) > 0 { + ste := <-started + if ste.CommandName != "find" { + return fmt.Errorf("human_4_1: found unexpected command started %s", ste.CommandName) + } + } + if len(succeeded) != 1 { + return fmt.Errorf("human_4_1: expected 1 finds succeed, found %d", len(succeeded)) + } + for len(succeeded) > 0 { + sue := <-succeeded + if sue.CommandName != "find" { + return fmt.Errorf("human_4_1: found unexpected command succeeded %s", sue.CommandName) + } + } + if len(failed) != 1 { + return fmt.Errorf("human_4_1: expected 1 finds succeed, found %d", len(failed)) + } + for len(failed) > 0 { + fe := <-failed + if fe.CommandName != "find" { + return fmt.Errorf("human_4_1: found unexpected command failed %s", fe.CommandName) + } + } + + return callbackFailed } func human42ReauthenticationSucceedsNoRefreshToken() error { @@ -1274,7 +1487,7 @@ func human42ReauthenticationSucceedsNoRefreshToken() error { var callbackFailed error countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("human_4_2: failed connecting admin client: %v", err) } @@ -1351,7 +1564,7 @@ func human43ReauthenticationSucceedsAfterRefreshFails() error { var callbackFailed error countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("human_4_3: failed connecting admin client: %v", err) } @@ -1429,7 +1642,7 @@ func human44ReauthenticationFails() error { var callbackFailed error countMutex := sync.Mutex{} - adminClient, err := connectAdminClinet() + adminClient, err := connectAdminClient() if err != nil { return fmt.Errorf("human_4_4: failed connecting admin client: %v", err) } diff --git a/mongo/options/clientoptions.go b/mongo/options/clientoptions.go index 4c7340a8ea..d2cb6cf40d 100644 --- a/mongo/options/clientoptions.go +++ b/mongo/options/clientoptions.go @@ -369,6 +369,9 @@ func (c *ClientOptions) validate() error { if c.Auth.OIDCMachineCallback != nil && c.Auth.OIDCHumanCallback != nil { return fmt.Errorf("cannot set both OIDCMachineCallback and OIDCHumanCallback, only one may be specified") } + if c.Auth.OIDCHumanCallback == nil && c.Auth.AuthMechanismProperties[auth.AllowedHostsProp] != "" { + return fmt.Errorf("Cannot specify ALLOWED_HOSTS without an OIDCHumanCallback") + } if env, ok := c.Auth.AuthMechanismProperties[auth.EnvironmentProp]; ok { switch env { case auth.GCPEnvironmentValue, auth.AzureEnvironmentValue: diff --git a/mongo/options/clientoptions_test.go b/mongo/options/clientoptions_test.go index d60d4498f2..ac94637323 100644 --- a/mongo/options/clientoptions_test.go +++ b/mongo/options/clientoptions_test.go @@ -588,6 +588,18 @@ func TestClientOptions(t *testing.T) { HTTPClient: httputil.DefaultHTTPClient, }, }, + { + "tmp", + "mongodb://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=TOKEN_RESOURCE:mongodb://test-cluster,ENVIRONMENT:azureManagedIdentities", + &ClientOptions{ + Hosts: []string{"example.com"}, + Auth: &Credential{AuthMechanism: "MONGODB-OIDC", AuthSource: "$external", AuthMechanismProperties: map[string]string{ + "ENVIRONMENT": "azureManagedIdentities", + "TOKEN_RESOURCE": "mongodb://test-cluster"}}, + err: nil, + HTTPClient: httputil.DefaultHTTPClient, + }, + }, { "comma in key:value pair causes error", "mongodb://example.com/?authMechanismProperties=TOKEN_RESOURCE:mongodb://host1%2Chost2", @@ -848,6 +860,14 @@ func TestClientOptions(t *testing.T) { OIDCMachineCallback: emptyCb, OIDCHumanCallback: emptyCb}), err: fmt.Errorf("cannot set both OIDCMachineCallback and OIDCHumanCallback, only one may be specified"), }, + { + name: "cannot set ALLOWED_HOSTS without OIDCHumanCallback", + opts: Client().SetAuth(Credential{AuthMechanism: "MONGODB-OIDC", + OIDCMachineCallback: emptyCb, + AuthMechanismProperties: map[string]string{"ALLOWED_HOSTS": "www.example.com"}, + }), + err: fmt.Errorf("Cannot specify ALLOWED_HOSTS without an OIDCHumanCallback"), + }, { name: "cannot set OIDCMachineCallback in GCP Environment", opts: Client().SetAuth(Credential{ diff --git a/x/mongo/driver/auth/oidc.go b/x/mongo/driver/auth/oidc.go index 0b71533b73..dbdfadf699 100644 --- a/x/mongo/driver/auth/oidc.go +++ b/x/mongo/driver/auth/oidc.go @@ -400,6 +400,7 @@ func (oa *OIDCAuthenticator) getAccessToken( // always set the IdPInfo, in most cases, this should just be recopying the same pointer, or nil // in the machine flow. oa.idpInfo = args.IDPInfo + return cred.AccessToken, nil }