diff --git a/sdk/azidentity/CHANGELOG.md b/sdk/azidentity/CHANGELOG.md index 872352e90d88..d3b70fe7e23f 100644 --- a/sdk/azidentity/CHANGELOG.md +++ b/sdk/azidentity/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.3.0-beta.4 (Unreleased) ### Features Added +* Added `WorkloadIdentityCredentialOptions.AdditionallyAllowedTenants` and `.DisableInstanceDiscovery` ### Breaking Changes diff --git a/sdk/azidentity/azidentity_test.go b/sdk/azidentity/azidentity_test.go index 721fdafeb52a..b6b835ef9268 100644 --- a/sdk/azidentity/azidentity_test.go +++ b/sdk/azidentity/azidentity_test.go @@ -13,6 +13,7 @@ import ( "fmt" "net/http" "os" + "path/filepath" "strings" "testing" @@ -278,6 +279,10 @@ func Test_NonHTTPSAuthorityHost(t *testing.T) { } func TestAdditionallyAllowedTenants(t *testing.T) { + af := filepath.Join(t.TempDir(), t.Name()+credNameWorkloadIdentity) + if err := os.WriteFile(af, []byte("assertion"), os.ModePerm); err != nil { + t.Fatal(err) + } tenantA := "A" tenantB := "B" for _, test := range []struct { @@ -385,6 +390,13 @@ func TestAdditionallyAllowedTenants(t *testing.T) { return NewUsernamePasswordCredential(fakeTenantID, fakeClientID, fakeUsername, "password", &o) }, }, + { + name: credNameWorkloadIdentity, + ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) { + o := WorkloadIdentityCredentialOptions{AdditionallyAllowedTenants: test.allowed, ClientOptions: co} + return NewWorkloadIdentityCredential(fakeTenantID, fakeClientID, af, &o) + }, + }, { name: "DefaultAzureCredential/EnvironmentCredential", ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) { @@ -411,6 +423,20 @@ func TestAdditionallyAllowedTenants(t *testing.T) { azureTenantID: fakeTenantID, }, }, + { + name: "DefaultAzureCredential/" + credNameWorkloadIdentity, + ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) { + o := DefaultAzureCredentialOptions{AdditionallyAllowedTenants: test.allowed, ClientOptions: co} + return NewDefaultAzureCredential(&o) + }, + env: map[string]string{ + azureAdditionallyAllowedTenants: strings.Join(test.allowed, ";"), + azureAuthorityHost: "https://login.microsoftonline.com", + azureClientID: fakeClientID, + azureFederatedTokenFile: af, + azureTenantID: fakeTenantID, + }, + }, { name: "EnvironmentCredential/" + credNameCert, ctor: func(co azcore.ClientOptions) (azcore.TokenCredential, error) { diff --git a/sdk/azidentity/default_azure_credential.go b/sdk/azidentity/default_azure_credential.go index 90ad3e6d60a1..a59fbe7e2b14 100644 --- a/sdk/azidentity/default_azure_credential.go +++ b/sdk/azidentity/default_azure_credential.go @@ -88,8 +88,10 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default if tenantID, ok := os.LookupEnv(azureTenantID); ok { haveWorkloadConfig = true workloadCred, err := NewWorkloadIdentityCredential(tenantID, clientID, file, &WorkloadIdentityCredentialOptions{ - ClientOptions: options.ClientOptions}, - ) + AdditionallyAllowedTenants: additionalTenants, + ClientOptions: options.ClientOptions, + DisableInstanceDiscovery: options.DisableInstanceDiscovery, + }) if err == nil { creds = append(creds, workloadCred) } else { diff --git a/sdk/azidentity/testdata/recordings/TestWorkloadIdentityCredential_Live/default_options.json b/sdk/azidentity/testdata/recordings/TestWorkloadIdentityCredential_Live/default_options.json new file mode 100644 index 000000000000..bbde112fa48d --- /dev/null +++ b/sdk/azidentity/testdata/recordings/TestWorkloadIdentityCredential_Live/default_options.json @@ -0,0 +1,211 @@ +{ + "Entries": [ + { + "RequestUri": "https://login.microsoftonline.com/common/discovery/instance?api-version=1.1\u0026authorization_endpoint=https%3A%2F%2Flogin.microsoftonline.com%2Ffake-tenant%2Foauth2%2Fv2.0%2Fauthorize", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept-Encoding": "gzip", + "User-Agent": "azsdk-go-azidentity/v1.3.0-beta.3 (go1.19.3; linux)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "955", + "Content-Type": "application/json; charset=utf-8", + "Date": "Wed, 22 Feb 2023 16:41:27 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Ai4jrsK9TAxJka4WMAs5ibsoaHJ6AwAAAN87iNsOAAAA; expires=Fri, 24-Mar-2023 16:41:28 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.14601.11 - WUS2 ProdSlices", + "x-ms-request-id": "3a446783-a7c9-494a-b8b5-093ed6c54c00", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "tenant_discovery_endpoint": "https://login.microsoftonline.com/fake-tenant/v2.0/.well-known/openid-configuration", + "api-version": "1.1", + "metadata": [ + { + "preferred_network": "login.microsoftonline.com", + "preferred_cache": "login.windows.net", + "aliases": [ + "login.microsoftonline.com", + "login.windows.net", + "login.microsoft.com", + "sts.windows.net" + ] + }, + { + "preferred_network": "login.partner.microsoftonline.cn", + "preferred_cache": "login.partner.microsoftonline.cn", + "aliases": [ + "login.partner.microsoftonline.cn", + "login.chinacloudapi.cn" + ] + }, + { + "preferred_network": "login.microsoftonline.de", + "preferred_cache": "login.microsoftonline.de", + "aliases": [ + "login.microsoftonline.de" + ] + }, + { + "preferred_network": "login.microsoftonline.us", + "preferred_cache": "login.microsoftonline.us", + "aliases": [ + "login.microsoftonline.us", + "login.usgovcloudapi.net" + ] + }, + { + "preferred_network": "login-us.microsoftonline.com", + "preferred_cache": "login-us.microsoftonline.com", + "aliases": [ + "login-us.microsoftonline.com" + ] + } + ] + } + }, + { + "RequestUri": "https://login.microsoftonline.com/fake-tenant/v2.0/.well-known/openid-configuration", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept-Encoding": "gzip", + "User-Agent": "azsdk-go-azidentity/v1.3.0-beta.3 (go1.19.3; linux)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "1578", + "Content-Security-Policy-Report-Only": "script-src \u0027self\u0027 \u0027nonce-xM6uwZGgkXjwOMuLR_EloA\u0027 \u0027unsafe-eval\u0027 \u0027unsafe-inline\u0027 \u0027report-sample\u0027; object-src \u0027none\u0027; base-uri \u0027self\u0027; report-uri https://csp.microsoft.com/report/ESTS-UX-All", + "Content-Type": "application/json; charset=utf-8", + "Date": "Wed, 22 Feb 2023 16:41:27 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Ai4jrsK9TAxJka4WMAs5ibsoaHJ6AwAAAN87iNsOAAAA; expires=Fri, 24-Mar-2023 16:41:28 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.14711.5 - WUS2 ProdSlices", + "x-ms-request-id": "e18db58a-9fac-4188-a706-46e0726f1800", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "token_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "private_key_jwt", + "client_secret_basic" + ], + "jwks_uri": "https://login.microsoftonline.com/fake-tenant/discovery/v2.0/keys", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "response_types_supported": [ + "code", + "id_token", + "code id_token", + "id_token token" + ], + "scopes_supported": [ + "openid", + "profile", + "email", + "offline_access" + ], + "issuer": "https://login.microsoftonline.com/fake-tenant/v2.0", + "request_uri_parameter_supported": false, + "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", + "authorization_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/authorize", + "device_authorization_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/devicecode", + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/logout", + "claims_supported": [ + "sub", + "iss", + "cloud_instance_name", + "cloud_instance_host_name", + "cloud_graph_host_name", + "msgraph_host", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "nonce", + "preferred_username", + "name", + "tid", + "ver", + "at_hash", + "c_hash", + "email" + ], + "kerberos_endpoint": "https://login.microsoftonline.com/fake-tenant/kerberos", + "tenant_region_scope": "WW", + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com", + "rbac_url": "https://pas.windows.net" + } + }, + { + "RequestUri": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/token", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept-Encoding": "gzip", + "Content-Length": "2", + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + "User-Agent": "azsdk-go-azidentity/v1.3.0-beta.3 (go1.19.3; linux)" + }, + "RequestBody": {}, + "StatusCode": 200, + "ResponseHeaders": { + "Cache-Control": "no-store, no-cache", + "Content-Length": "91", + "Content-Type": "application/json; charset=utf-8", + "Date": "Wed, 22 Feb 2023 16:41:28 GMT", + "Expires": "-1", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Pragma": "no-cache", + "Set-Cookie": [ + "fpc=Ai4jrsK9TAxJka4WMAs5ibsoaHJ6BAAAAN87iNsOAAAA; expires=Fri, 24-Mar-2023 16:41:28 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.14649.20 - SCUS ProdSlices", + "x-ms-request-id": "b4c3978a-1708-407e-a57a-4c1ff2197900", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "token_type": "Bearer", + "expires_in": 86399, + "ext_expires_in": 86399, + "access_token": "redacted" + } + } + ], + "Variables": {} +} diff --git a/sdk/azidentity/testdata/recordings/TestWorkloadIdentityCredential_Live/instance_discovery_disabled.json b/sdk/azidentity/testdata/recordings/TestWorkloadIdentityCredential_Live/instance_discovery_disabled.json new file mode 100644 index 000000000000..cd5833fc385c --- /dev/null +++ b/sdk/azidentity/testdata/recordings/TestWorkloadIdentityCredential_Live/instance_discovery_disabled.json @@ -0,0 +1,136 @@ +{ + "Entries": [ + { + "RequestUri": "https://login.microsoftonline.com/fake-tenant/v2.0/.well-known/openid-configuration", + "RequestMethod": "GET", + "RequestHeaders": { + "Accept-Encoding": "gzip", + "User-Agent": "azsdk-go-azidentity/v1.3.0-beta.3 (go1.19.3; linux)" + }, + "RequestBody": null, + "StatusCode": 200, + "ResponseHeaders": { + "Access-Control-Allow-Methods": "GET, OPTIONS", + "Access-Control-Allow-Origin": "*", + "Cache-Control": "max-age=86400, private", + "Content-Length": "1578", + "Content-Type": "application/json; charset=utf-8", + "Date": "Wed, 22 Feb 2023 16:41:27 GMT", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Set-Cookie": [ + "fpc=Ai4jrsK9TAxJka4WMAs5ibsoaHJ6AgAAAN87iNsOAAAA; expires=Fri, 24-Mar-2023 16:41:27 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.14649.20 - EUS ProdSlices", + "x-ms-request-id": "214bb59b-81a7-4925-8fed-c009819c3c00", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "token_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/token", + "token_endpoint_auth_methods_supported": [ + "client_secret_post", + "private_key_jwt", + "client_secret_basic" + ], + "jwks_uri": "https://login.microsoftonline.com/fake-tenant/discovery/v2.0/keys", + "response_modes_supported": [ + "query", + "fragment", + "form_post" + ], + "subject_types_supported": [ + "pairwise" + ], + "id_token_signing_alg_values_supported": [ + "RS256" + ], + "response_types_supported": [ + "code", + "id_token", + "code id_token", + "id_token token" + ], + "scopes_supported": [ + "openid", + "profile", + "email", + "offline_access" + ], + "issuer": "https://login.microsoftonline.com/fake-tenant/v2.0", + "request_uri_parameter_supported": false, + "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo", + "authorization_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/authorize", + "device_authorization_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/devicecode", + "http_logout_supported": true, + "frontchannel_logout_supported": true, + "end_session_endpoint": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/logout", + "claims_supported": [ + "sub", + "iss", + "cloud_instance_name", + "cloud_instance_host_name", + "cloud_graph_host_name", + "msgraph_host", + "aud", + "exp", + "iat", + "auth_time", + "acr", + "nonce", + "preferred_username", + "name", + "tid", + "ver", + "at_hash", + "c_hash", + "email" + ], + "kerberos_endpoint": "https://login.microsoftonline.com/fake-tenant/kerberos", + "tenant_region_scope": "WW", + "cloud_instance_name": "microsoftonline.com", + "cloud_graph_host_name": "graph.windows.net", + "msgraph_host": "graph.microsoft.com", + "rbac_url": "https://pas.windows.net" + } + }, + { + "RequestUri": "https://login.microsoftonline.com/fake-tenant/oauth2/v2.0/token", + "RequestMethod": "POST", + "RequestHeaders": { + "Accept-Encoding": "gzip", + "Content-Length": "2", + "Content-Type": "application/x-www-form-urlencoded; charset=utf-8", + "User-Agent": "azsdk-go-azidentity/v1.3.0-beta.3 (go1.19.3; linux)" + }, + "RequestBody": {}, + "StatusCode": 200, + "ResponseHeaders": { + "Cache-Control": "no-store, no-cache", + "Content-Length": "91", + "Content-Type": "application/json; charset=utf-8", + "Date": "Wed, 22 Feb 2023 16:41:27 GMT", + "Expires": "-1", + "P3P": "CP=\u0022DSP CUR OTPi IND OTRi ONL FIN\u0022", + "Pragma": "no-cache", + "Set-Cookie": [ + "fpc=Ai4jrsK9TAxJka4WMAs5ibsoaHJ6AwAAAN87iNsOAAAA; expires=Fri, 24-Mar-2023 16:41:28 GMT; path=/; secure; HttpOnly; SameSite=None", + "x-ms-gateway-slice=estsfd; path=/; secure; samesite=none; httponly" + ], + "Strict-Transport-Security": "max-age=31536000; includeSubDomains", + "X-Content-Type-Options": "nosniff", + "x-ms-ests-server": "2.1.14649.20 - NCUS ProdSlices", + "x-ms-request-id": "bf6be5e4-8ed1-4eec-9b63-c9886d4d8b00", + "X-XSS-Protection": "0" + }, + "ResponseBody": { + "token_type": "Bearer", + "expires_in": 86399, + "ext_expires_in": 86399, + "access_token": "redacted" + } + } + ], + "Variables": {} +} diff --git a/sdk/azidentity/workload_identity.go b/sdk/azidentity/workload_identity.go index e6af670f78f4..a33e77cc03fd 100644 --- a/sdk/azidentity/workload_identity.go +++ b/sdk/azidentity/workload_identity.go @@ -32,6 +32,13 @@ type WorkloadIdentityCredential struct { // WorkloadIdentityCredentialOptions contains optional parameters for WorkloadIdentityCredential. type WorkloadIdentityCredentialOptions struct { azcore.ClientOptions + + // AdditionallyAllowedTenants specifies additional tenants for which the credential may acquire tokens. + // Add the wildcard value "*" to allow the credential to acquire tokens for any tenant in which the + // application is registered. + AdditionallyAllowedTenants []string + // DisableInstanceDiscovery allows disconnected cloud solutions to skip instance discovery for unknown authority hosts. + DisableInstanceDiscovery bool } // NewWorkloadIdentityCredential constructs a WorkloadIdentityCredential. tenantID and clientID specify the identity the credential authenticates. @@ -41,7 +48,12 @@ func NewWorkloadIdentityCredential(tenantID, clientID, file string, options *Wor options = &WorkloadIdentityCredentialOptions{} } w := WorkloadIdentityCredential{file: file, mtx: &sync.RWMutex{}} - cred, err := NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &ClientAssertionCredentialOptions{ClientOptions: options.ClientOptions}) + caco := ClientAssertionCredentialOptions{ + AdditionallyAllowedTenants: options.AdditionallyAllowedTenants, + ClientOptions: options.ClientOptions, + DisableInstanceDiscovery: options.DisableInstanceDiscovery, + } + cred, err := NewClientAssertionCredential(tenantID, clientID, w.getAssertion, &caco) if err != nil { return nil, err } diff --git a/sdk/azidentity/workload_identity_test.go b/sdk/azidentity/workload_identity_test.go index 0b8979ffc5d6..dccbe0d7eae4 100644 --- a/sdk/azidentity/workload_identity_test.go +++ b/sdk/azidentity/workload_identity_test.go @@ -8,18 +8,79 @@ package azidentity import ( "context" + "crypto" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/json" "fmt" "net/http" "os" "path/filepath" + "strconv" "strings" "testing" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/internal/mock" + "github.com/golang-jwt/jwt/v4" + "github.com/google/uuid" ) +func getAssertion(cert *x509.Certificate, key crypto.PrivateKey) (string, error) { + j := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{ + "aud": fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", liveSP.tenantID), + "exp": json.Number(strconv.FormatInt(time.Now().Add(10*time.Minute).Unix(), 10)), + "iss": liveSP.clientID, + "jti": uuid.New().String(), + "nbf": json.Number(strconv.FormatInt(time.Now().Unix(), 10)), + "sub": liveSP.clientID, + }) + x5t := sha1.Sum(cert.Raw) // nosec + j.Header = map[string]interface{}{ + "alg": "RS256", + "typ": "JWT", + "x5t": base64.StdEncoding.EncodeToString(x5t[:]), + } + return j.SignedString(key) +} + +func TestWorkloadIdentityCredential_Live(t *testing.T) { + cert, err := os.ReadFile(liveSP.pemPath) + if err != nil { + t.Fatal(err) + } + certs, key, err := ParseCertificates(cert, nil) + if err != nil { + t.Fatal(err) + } + a, err := getAssertion(certs[0], key) + if err != nil { + t.Fatal(err) + } + f := filepath.Join(t.TempDir(), t.Name()) + if err := os.WriteFile(f, []byte(a), os.ModePerm); err != nil { + t.Fatalf("failed to write token file: %v", err) + } + for _, b := range []bool{true, false} { + name := "default options" + if b { + name = "instance discovery disabled" + } + t.Run(name, func(t *testing.T) { + co, stop := initRecording(t) + defer stop() + o := WorkloadIdentityCredentialOptions{ClientOptions: co, DisableInstanceDiscovery: b} + cred, err := NewWorkloadIdentityCredential(liveSP.tenantID, liveSP.clientID, f, &o) + if err != nil { + t.Fatal(err) + } + testGetTokenSuccess(t, cred) + }) + } +} + func TestWorkloadIdentityCredential(t *testing.T) { tempFile := filepath.Join(t.TempDir(), "test-workload-token-file") if err := os.WriteFile(tempFile, []byte(tokenValue), os.ModePerm); err != nil {