Skip to content

Commit 9a02911

Browse files
GODRIVER-2728: Implement automatic Azure token acquisition callback (#1703)
Co-authored-by: Matt Dale <[email protected]>
1 parent 45f9059 commit 9a02911

File tree

4 files changed

+166
-15
lines changed

4 files changed

+166
-15
lines changed

.evergreen/config.yml

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ functions:
365365
script: |
366366
${PREPARE_SHELL}
367367
export OIDC="oidc"
368-
bash ${PROJECT_DIRECTORY}/etc/run-oidc-test.sh
368+
bash ${PROJECT_DIRECTORY}/etc/run-oidc-test.sh 'make -s evg-test-oidc-auth'
369369
370370
run-make:
371371
- command: shell.exec
@@ -1975,6 +1975,31 @@ tasks:
19751975
commands:
19761976
- func: "run-oidc-auth-test-with-test-credentials"
19771977

1978+
- name: "oidc-auth-test-azure-latest"
1979+
commands:
1980+
- command: shell.exec
1981+
params:
1982+
working_dir: src/go.mongodb.org/mongo-driver
1983+
shell: bash
1984+
script: |-
1985+
set -o errexit
1986+
${PREPARE_SHELL}
1987+
export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-go-driver.tar.gz
1988+
# we need to statically link libc to avoid the situation where the VM has a different
1989+
# version of libc
1990+
go build -tags osusergo,netgo -ldflags '-w -extldflags "-static -lgcc -lc"' -o test ./cmd/testoidcauth/main.go
1991+
rm "$AZUREOIDC_DRIVERS_TAR_FILE" || true
1992+
tar -cf $AZUREOIDC_DRIVERS_TAR_FILE ./test
1993+
tar -uf $AZUREOIDC_DRIVERS_TAR_FILE ./etc
1994+
rm "$AZUREOIDC_DRIVERS_TAR_FILE".gz || true
1995+
gzip $AZUREOIDC_DRIVERS_TAR_FILE
1996+
export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/mongo-go-driver.tar.gz
1997+
# Define the command to run on the azure VM.
1998+
# Ensure that we source the environment file created for us, set up any other variables we need,
1999+
# and then run our test suite on the vm.
2000+
export AZUREOIDC_TEST_CMD="PROJECT_DIRECTORY='.' OIDC_ENV=azure OIDC=oidc ./etc/run-oidc-test.sh ./test"
2001+
bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh
2002+
19782003
- name: "test-search-index"
19792004
commands:
19802005
- func: "bootstrap-mongo-orchestration"
@@ -2293,6 +2318,30 @@ task_groups:
22932318
tasks:
22942319
- oidc-auth-test-latest
22952320

2321+
- name: testazureoidc_task_group
2322+
setup_group:
2323+
- func: fetch-source
2324+
- func: prepare-resources
2325+
- func: fix-absolute-paths
2326+
- func: make-files-executable
2327+
- command: subprocess.exec
2328+
params:
2329+
binary: bash
2330+
env:
2331+
AZUREOIDC_VMNAME_PREFIX: "GO_DRIVER"
2332+
args:
2333+
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/create-and-setup-vm.sh
2334+
teardown_task:
2335+
- command: subprocess.exec
2336+
params:
2337+
binary: bash
2338+
args:
2339+
- ${DRIVERS_TOOLS}/.evergreen/auth_oidc/azure/delete-vm.sh
2340+
setup_group_can_fail_task: true
2341+
setup_group_timeout_secs: 1800
2342+
tasks:
2343+
- oidc-auth-test-azure-latest
2344+
22962345
- name: test-aws-lambda-task-group
22972346
setup_group:
22982347
- func: fetch-source
@@ -2642,3 +2691,5 @@ buildvariants:
26422691
tasks:
26432692
- name: testoidc_task_group
26442693
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README
2694+
- name: testazureoidc_task_group
2695+
batchtime: 20160 # Use a batchtime of 14 days as suggested by the CSFLE test README

cmd/testoidcauth/main.go

Lines changed: 61 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,17 +74,26 @@ func main() {
7474
fmt.Println("...Ok")
7575
}
7676
}
77-
aux("machine_1_1_callbackIsCalled", machine11callbackIsCalled)
78-
aux("machine_1_2_callbackIsCalledOnlyOneForMultipleConnections", machine12callbackIsCalledOnlyOneForMultipleConnections)
79-
aux("machine_2_1_validCallbackInputs", machine21validCallbackInputs)
80-
aux("machine_2_3_oidcCallbackReturnMissingData", machine23oidcCallbackReturnMissingData)
81-
aux("machine_2_4_invalidClientConfigurationWithCallback", machine24invalidClientConfigurationWithCallback)
82-
aux("machine_3_1_failureWithCachedTokensFetchANewTokenAndRetryAuth", machine31failureWithCachedTokensFetchANewTokenAndRetryAuth)
83-
aux("machine_3_2_authFailuresWithoutCachedTokensReturnsAnError", machine32authFailuresWithoutCachedTokensReturnsAnError)
84-
aux("machine_3_3_UnexpectedErrorCodeDoesNotClearTheCache", machine33UnexpectedErrorCodeDoesNotClearTheCache)
85-
aux("machine_4_1_reauthenticationSucceeds", machine41ReauthenticationSucceeds)
86-
aux("machine_4_2_readCommandsFailIfReauthenticationFails", machine42ReadCommandsFailIfReauthenticationFails)
87-
aux("machine_4_3_writeCommandsFailIfReauthenticationFails", machine43WriteCommandsFailIfReauthenticationFails)
77+
env := os.Getenv("OIDC_ENV")
78+
switch env {
79+
case "":
80+
aux("machine_1_1_callbackIsCalled", machine11callbackIsCalled)
81+
aux("machine_1_2_callbackIsCalledOnlyOneForMultipleConnections", machine12callbackIsCalledOnlyOneForMultipleConnections)
82+
aux("machine_2_1_validCallbackInputs", machine21validCallbackInputs)
83+
aux("machine_2_3_oidcCallbackReturnMissingData", machine23oidcCallbackReturnMissingData)
84+
aux("machine_2_4_invalidClientConfigurationWithCallback", machine24invalidClientConfigurationWithCallback)
85+
aux("machine_3_1_failureWithCachedTokensFetchANewTokenAndRetryAuth", machine31failureWithCachedTokensFetchANewTokenAndRetryAuth)
86+
aux("machine_3_2_authFailuresWithoutCachedTokensReturnsAnError", machine32authFailuresWithoutCachedTokensReturnsAnError)
87+
aux("machine_3_3_UnexpectedErrorCodeDoesNotClearTheCache", machine33UnexpectedErrorCodeDoesNotClearTheCache)
88+
aux("machine_4_1_reauthenticationSucceeds", machine41ReauthenticationSucceeds)
89+
aux("machine_4_2_readCommandsFailIfReauthenticationFails", machine42ReadCommandsFailIfReauthenticationFails)
90+
aux("machine_4_3_writeCommandsFailIfReauthenticationFails", machine43WriteCommandsFailIfReauthenticationFails)
91+
case "azure":
92+
aux("machine_5_1_azureWithNoUsername", machine51azureWithNoUsername)
93+
aux("machine_5_2_azureWithNoUsername", machine52azureWithBadUsername)
94+
default:
95+
log.Fatal("Unknown OIDC_ENV: ", env)
96+
}
8897
if hasError {
8998
log.Fatal("One or more tests failed")
9099
}
@@ -686,3 +695,44 @@ func machine43WriteCommandsFailIfReauthenticationFails() error {
686695
}
687696
return callbackFailed
688697
}
698+
699+
func machine51azureWithNoUsername() error {
700+
opts := options.Client().ApplyURI(uriSingle)
701+
if opts == nil || opts.Auth == nil {
702+
return fmt.Errorf("machine_5_1: failed parsing uri: %q", uriSingle)
703+
}
704+
client, err := mongo.Connect(context.Background(), opts)
705+
if err != nil {
706+
return fmt.Errorf("machine_5_1: failed connecting client: %v", err)
707+
}
708+
defer client.Disconnect(context.Background())
709+
710+
coll := client.Database("test").Collection("test")
711+
712+
_, err = coll.Find(context.Background(), bson.D{})
713+
if err != nil {
714+
return fmt.Errorf("machine_5_1: failed executing Find: %v", err)
715+
}
716+
return nil
717+
}
718+
719+
func machine52azureWithBadUsername() error {
720+
opts := options.Client().ApplyURI(uriSingle)
721+
if opts == nil || opts.Auth == nil {
722+
return fmt.Errorf("machine_5_2: failed parsing uri: %q", uriSingle)
723+
}
724+
opts.Auth.Username = "bad"
725+
client, err := mongo.Connect(context.Background(), opts)
726+
if err != nil {
727+
return fmt.Errorf("machine_5_2: failed connecting client: %v", err)
728+
}
729+
defer client.Disconnect(context.Background())
730+
731+
coll := client.Database("test").Collection("test")
732+
733+
_, err = coll.Find(context.Background(), bson.D{})
734+
if err == nil {
735+
return fmt.Errorf("machine_5_2: Find succeeded when it should fail")
736+
}
737+
return nil
738+
}

etc/run-oidc-test.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,4 @@ export TEST_AUTH_OIDC=1
3030
export COVERAGE=1
3131
export AUTH="auth"
3232

33-
make -s evg-test-oidc-auth
33+
$1

x/mongo/driver/auth/oidc.go

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ package auth
88

99
import (
1010
"context"
11+
"encoding/json"
1112
"fmt"
1213
"net/http"
14+
"net/url"
1315
"strings"
1416
"sync"
1517
"time"
@@ -166,10 +168,15 @@ func (oa *OIDCAuthenticator) providerCallback() (OIDCCallback, error) {
166168
}
167169

168170
switch env {
169-
// TODO GODRIVER-2728: Automatic token acquisition for Azure Identity Provider
171+
case azureEnvironmentValue:
172+
resource, ok := oa.AuthMechanismProperties[resourceProp]
173+
if !ok {
174+
return nil, newAuthError(fmt.Sprintf("%q must be specified for Azure OIDC", resourceProp), nil)
175+
}
176+
return getAzureOIDCCallback(oa.userName, resource, oa.httpClient), nil
170177
// TODO GODRIVER-2806: Automatic token acquisition for GCP Identity Provider
171178
// This is here just to pass the linter, it will be fixed in one of the above tickets.
172-
case azureEnvironmentValue, gcpEnvironmentValue:
179+
case gcpEnvironmentValue:
173180
return func(ctx context.Context, args *OIDCArgs) (*OIDCCredential, error) {
174181
return nil, fmt.Errorf("automatic token acquisition for %q not implemented yet", env)
175182
}, fmt.Errorf("automatic token acquisition for %q not implemented yet", env)
@@ -178,6 +185,49 @@ func (oa *OIDCAuthenticator) providerCallback() (OIDCCallback, error) {
178185
return nil, fmt.Errorf("%q %q not supported for MONGODB-OIDC", environmentProp, env)
179186
}
180187

188+
// getAzureOIDCCallback returns the callback for the Azure Identity Provider.
189+
func getAzureOIDCCallback(clientID string, resource string, httpClient *http.Client) OIDCCallback {
190+
// return the callback parameterized by the clientID and resource, also passing in the user
191+
// configured httpClient.
192+
return func(ctx context.Context, args *OIDCArgs) (*OIDCCredential, error) {
193+
resource = url.QueryEscape(resource)
194+
var uri string
195+
if clientID != "" {
196+
uri = fmt.Sprintf("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=%s&client_id=%s", resource, clientID)
197+
} else {
198+
uri = fmt.Sprintf("http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=%s", resource)
199+
}
200+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil)
201+
if err != nil {
202+
return nil, newAuthError("error creating http request to Azure Identity Provider", err)
203+
}
204+
req.Header.Add("Metadata", "true")
205+
req.Header.Add("Accept", "application/json")
206+
resp, err := httpClient.Do(req)
207+
if err != nil {
208+
return nil, newAuthError("error getting access token from Azure Identity Provider", err)
209+
}
210+
defer resp.Body.Close()
211+
var azureResp struct {
212+
AccessToken string `json:"access_token"`
213+
ExpiresOn int64 `json:"expires_on,string"`
214+
}
215+
216+
if resp.StatusCode != http.StatusOK {
217+
return nil, newAuthError(fmt.Sprintf("failed to get a valid response from Azure Identity Provider, http code: %d", resp.StatusCode), nil)
218+
}
219+
err = json.NewDecoder(resp.Body).Decode(&azureResp)
220+
if err != nil {
221+
return nil, newAuthError("failed parsing result from Azure Identity Provider", err)
222+
}
223+
expireTime := time.Unix(azureResp.ExpiresOn, 0)
224+
return &OIDCCredential{
225+
AccessToken: azureResp.AccessToken,
226+
ExpiresAt: &expireTime,
227+
}, nil
228+
}
229+
}
230+
181231
func (oa *OIDCAuthenticator) getAccessToken(
182232
ctx context.Context,
183233
conn driver.Connection,

0 commit comments

Comments
 (0)