Skip to content

Commit f8cb16d

Browse files
authored
Add WorkloadIdentityCredential (Azure#19503)
1 parent 27eba77 commit f8cb16d

12 files changed

+308
-25
lines changed

sdk/azidentity/CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
# Release History
22

3-
## 1.2.1 (Unreleased)
3+
## 1.3.0-beta.1 (Unreleased)
44

55
### Features Added
6+
* `WorkloadIdentityCredential` and `DefaultAzureCredential` support
7+
Workload Identity Federation on Kubernetes. `DefaultAzureCredential`
8+
support requires environment variable configuration as set by the
9+
Workload Identity webhook.
10+
([#15615](https://github.com/Azure/azure-sdk-for-go/issues/15615))
611

712
### Breaking Changes
813

sdk/azidentity/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,9 @@ an Azure AD access token. See [Credential Types](#credential-types "Credential T
5555
![DefaultAzureCredential authentication flow](img/mermaidjs/DefaultAzureCredentialAuthFlow.svg)
5656

5757
1. **Environment** - `DefaultAzureCredential` will read account information specified via [environment variables](#environment-variables) and use it to authenticate.
58-
2. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it.
59-
3. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity.
58+
1. **Workload Identity** - If the app is deployed on Kubernetes with environment variables set by the workload identity webhook, `DefaultAzureCredential` will authenticate the configured identity.
59+
1. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it.
60+
1. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity.
6061

6162
> Note: `DefaultAzureCredential` is intended to simplify getting started with the SDK by handling common scenarios with reasonable default behaviors. Developers who want more control or whose scenario isn't served by the default settings should use other credential types.
6263
@@ -128,6 +129,7 @@ client := armresources.NewResourceGroupsClient("subscription ID", chain, nil)
128129
|[ChainedTokenCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ChainedTokenCredential)|Define custom authentication flows, composing multiple credentials
129130
|[EnvironmentCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential)|Authenticate a service principal or user configured by environment variables
130131
|[ManagedIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential)|Authenticate the managed identity of an Azure resource
132+
|[WorkloadIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#WorkloadIdentityCredential)|Authenticate a workload identity on Kubernetes
131133

132134
### Authenticating Service Principals
133135

sdk/azidentity/azidentity.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030
azureClientCertificatePath = "AZURE_CLIENT_CERTIFICATE_PATH"
3131
azureClientID = "AZURE_CLIENT_ID"
3232
azureClientSecret = "AZURE_CLIENT_SECRET"
33+
azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE"
3334
azurePassword = "AZURE_PASSWORD"
3435
azureRegionalAuthorityName = "AZURE_REGIONAL_AUTHORITY_NAME"
3536
azureTenantID = "AZURE_TENANT_ID"

sdk/azidentity/client_assertion_credential.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ import (
1818
const credNameAssertion = "ClientAssertionCredential"
1919

2020
// ClientAssertionCredential authenticates an application with assertions provided by a callback function.
21-
// This credential is for advanced scenarios. ClientCertificateCredential has a more convenient API for
21+
// This credential is for advanced scenarios. [ClientCertificateCredential] has a more convenient API for
2222
// the most common assertion scenario, authenticating a service principal with a certificate. See
2323
// [Azure AD documentation] for details of the assertion format.
2424
//
2525
// [Azure AD documentation]: https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#assertion-format
2626
type ClientAssertionCredential struct {
2727
client confidentialClient
28+
// name enables replacing "ClientAssertionCredential" with "WorkloadIdentityCredential" in log messages
29+
name string
2830
}
2931

3032
// ClientAssertionCredentialOptions contains optional parameters for ClientAssertionCredential.
@@ -49,7 +51,7 @@ func NewClientAssertionCredential(tenantID, clientID string, getAssertion func(c
4951
if err != nil {
5052
return nil, err
5153
}
52-
return &ClientAssertionCredential{client: c}, nil
54+
return &ClientAssertionCredential{client: c, name: credNameAssertion}, nil
5355
}
5456

5557
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
@@ -59,15 +61,15 @@ func (c *ClientAssertionCredential) GetToken(ctx context.Context, opts policy.To
5961
}
6062
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
6163
if err == nil {
62-
logGetTokenSuccess(c, opts)
64+
logGetTokenSuccessImpl(c.name, opts)
6365
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
6466
}
6567

6668
ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes)
6769
if err != nil {
68-
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameAssertion, err)
70+
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(c.name, err)
6971
}
70-
logGetTokenSuccess(c, opts)
72+
logGetTokenSuccessImpl(c.name, opts)
7173
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
7274
}
7375

sdk/azidentity/default_azure_credential.go

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,15 @@ type DefaultAzureCredentialOptions struct {
3030

3131
// DefaultAzureCredential is a default credential chain for applications that will deploy to Azure.
3232
// It combines credentials suitable for deployment with credentials suitable for local development.
33-
// It attempts to authenticate with each of these credential types, in the following order, stopping when one provides a token:
33+
// It attempts to authenticate with each of these credential types, in the following order, stopping
34+
// when one provides a token:
3435
//
35-
// EnvironmentCredential
36-
// ManagedIdentityCredential
37-
// AzureCLICredential
36+
// - [EnvironmentCredential]
37+
// - [WorkloadIdentityCredential], if environment variable configuration is set by the Azure workload
38+
// identity webhook. Use [WorkloadIdentityCredential] directly when not using the webhook or needing
39+
// more control over its configuration.
40+
// - [ManagedIdentityCredential]
41+
// - [AzureCLICredential]
3842
//
3943
// Consult the documentation for these credential types for more information on how they authenticate.
4044
// Once a credential has successfully authenticated, DefaultAzureCredential will use that credential for
@@ -60,9 +64,35 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default
6064
creds = append(creds, &defaultCredentialErrorReporter{credType: "EnvironmentCredential", err: err})
6165
}
6266

67+
// workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID
68+
haveWorkloadConfig := false
69+
clientID, haveClientID := os.LookupEnv(azureClientID)
70+
if haveClientID {
71+
if file, ok := os.LookupEnv(azureFederatedTokenFile); ok {
72+
if _, ok := os.LookupEnv(azureAuthorityHost); ok {
73+
if tenantID, ok := os.LookupEnv(azureTenantID); ok {
74+
haveWorkloadConfig = true
75+
workloadCred, err := NewWorkloadIdentityCredential(tenantID, clientID, file, &WorkloadIdentityCredentialOptions{
76+
ClientOptions: options.ClientOptions},
77+
)
78+
if err == nil {
79+
creds = append(creds, workloadCred)
80+
} else {
81+
errorMessages = append(errorMessages, credNameWorkloadIdentity+": "+err.Error())
82+
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err})
83+
}
84+
}
85+
}
86+
}
87+
}
88+
if !haveWorkloadConfig {
89+
err := errors.New("missing environment variables for workload identity. Check webhook and pod configuration")
90+
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err})
91+
}
92+
6393
o := &ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions}
64-
if ID, ok := os.LookupEnv(azureClientID); ok {
65-
o.ID = ClientID(ID)
94+
if haveClientID {
95+
o.ID = ClientID(clientID)
6696
}
6797
msiCred, err := NewManagedIdentityCredential(o)
6898
if err == nil {

sdk/azidentity/default_azure_credential_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@ package azidentity
99
import (
1010
"context"
1111
"fmt"
12+
"net/http"
13+
"os"
14+
"path/filepath"
15+
"strings"
1216
"testing"
1317

18+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
1419
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
1520
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
21+
"github.com/Azure/azure-sdk-for-go/sdk/internal/mock"
1622
)
1723

1824
func TestDefaultAzureCredential_GetTokenSuccess(t *testing.T) {
@@ -89,3 +95,45 @@ func TestDefaultAzureCredential_UserAssignedIdentity(t *testing.T) {
8995
})
9096
}
9197
}
98+
99+
func TestDefaultAzureCredential_Workload(t *testing.T) {
100+
expectedAssertion := "service account token"
101+
tempFile := filepath.Join(t.TempDir(), "service-account-token-file")
102+
if err := os.WriteFile(tempFile, []byte(expectedAssertion), os.ModePerm); err != nil {
103+
t.Fatalf(`failed to write temporary file "%s": %v`, tempFile, err)
104+
}
105+
pred := func(req *http.Request) bool {
106+
if err := req.ParseForm(); err != nil {
107+
t.Fatal(err)
108+
}
109+
if actual := req.PostForm["client_assertion"]; actual[0] != expectedAssertion {
110+
t.Fatalf(`unexpected assertion "%s"`, actual[0])
111+
}
112+
if actual := req.PostForm["client_id"]; actual[0] != fakeClientID {
113+
t.Fatalf(`unexpected assertion "%s"`, actual[0])
114+
}
115+
if actual := strings.Split(req.URL.Path, "/")[1]; actual != fakeTenantID {
116+
t.Fatalf(`unexpected tenant "%s"`, actual)
117+
}
118+
return true
119+
}
120+
srv, close := mock.NewServer(mock.WithTransformAllRequestsToTestServerUrl())
121+
defer close()
122+
srv.AppendResponse(mock.WithBody(instanceDiscoveryResponse))
123+
srv.AppendResponse(mock.WithBody(tenantDiscoveryResponse))
124+
srv.AppendResponse(mock.WithPredicate(pred), mock.WithBody(accessTokenRespSuccess))
125+
srv.AppendResponse()
126+
for k, v := range map[string]string{
127+
azureAuthorityHost: cloud.AzurePublic.ActiveDirectoryAuthorityHost,
128+
azureClientID: fakeClientID,
129+
azureFederatedTokenFile: tempFile,
130+
azureTenantID: fakeTenantID,
131+
} {
132+
t.Setenv(k, v)
133+
}
134+
cred, err := NewDefaultAzureCredential(&DefaultAzureCredentialOptions{ClientOptions: policy.ClientOptions{Transport: srv}})
135+
if err != nil {
136+
t.Fatal(err)
137+
}
138+
testGetTokenSuccess(t, cred)
139+
}

sdk/azidentity/img/mermaidjs/DefaultAzureCredentialAuthFlow.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55
%% 2. Run command: mmdc -i DefaultAzureCredentialAuthFlow.md -o DefaultAzureCredentialAuthFlow.svg
66
77
flowchart LR;
8-
A(Environment):::deployed ==> B(Managed Identity):::deployed ==> C(Azure CLI):::developer;
8+
A(Environment):::deployed ==> B(Workload Identity):::deployed ==> C(Managed Identity):::deployed ==> D(Azure CLI):::developer;
99
1010
subgraph CREDENTIAL TYPES;
1111
direction LR;
1212
Deployed(Deployed service):::deployed ==> Developer(Developer):::developer;
1313
1414
%% Hide links between boxes in the legend by setting width to 0. The integers after "linkStyle" represent link indices.
15-
linkStyle 2 stroke-width:0px;
15+
linkStyle 3 stroke-width:0px;
1616
end;
1717
1818
%% Define styles for credential type boxes
@@ -21,6 +21,7 @@ flowchart LR;
2121
2222
%% Add API ref links to credential type boxes
2323
click A "https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential" _blank;
24-
click B "https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential" _blank;
25-
click C "https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#AzureCLICredential" _blank;
24+
click B "https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#WorkloadIdentityCredential" _blank;
25+
click C "https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential" _blank;
26+
click D "https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#AzureCLICredential" _blank;
2627
```

0 commit comments

Comments
 (0)