Skip to content

Commit ab1dd7f

Browse files
authored
Add Azure spiffe oidc auth profile (#3797)
Signed-off-by: Jonathan Collinge <[email protected]>
1 parent ca1b620 commit ab1dd7f

File tree

6 files changed

+134
-9
lines changed

6 files changed

+134
-9
lines changed

common/authentication/aws/x509.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ func (a *x509) Close() error {
143143

144144
func (a *x509) getCertPEM(ctx context.Context) error {
145145
// retrieve svid from spiffe context
146-
svid, ok := spiffecontext.From(ctx)
146+
svid, ok := spiffecontext.X509From(ctx)
147147
if !ok {
148148
return errors.New("no SVID found in context")
149149
}

common/authentication/aws/x509_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func TestGetX509Client(t *testing.T) {
117117
require.NoError(t, err)
118118

119119
// inject the SVID source into the context
120-
ctx = spiffecontext.With(ctx, s)
120+
ctx = spiffecontext.WithX509(ctx, s.X509SVIDSource())
121121
session, err := mockAWS.createOrRefreshSession(ctx)
122122

123123
require.NoError(t, err)

common/authentication/azure/auth.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,17 @@ func (s EnvironmentSettings) addWorkloadIdentityProvider(creds *[]azcore.TokenCr
139139
}
140140
}
141141

142+
func (s EnvironmentSettings) addSpiffeWorkloadIdentityProvider(creds *[]azcore.TokenCredential, errs *[]error) {
143+
if c, e := s.GetSpiffeWorkloadIdentity(); e == nil {
144+
cred, err := c.GetTokenCredential()
145+
if err == nil {
146+
*creds = append(*creds, cred)
147+
} else {
148+
*errs = append(*errs, err)
149+
}
150+
}
151+
}
152+
142153
func (s EnvironmentSettings) addManagedIdentityProvider(timeout time.Duration, creds *[]azcore.TokenCredential, errs *[]error) {
143154
c := s.GetMSI()
144155
msiCred, err := c.GetTokenCredential()
@@ -172,6 +183,8 @@ func (s EnvironmentSettings) addProviderByAuthMethodName(authMethod string, cred
172183
s.addClientCertificateProvider(creds, errs)
173184
case "workloadidentity", "wi":
174185
s.addWorkloadIdentityProvider(creds, errs)
186+
case "spiffeworkloadidentity", "spiffe":
187+
s.addSpiffeWorkloadIdentityProvider(creds, errs)
175188
case "managedidentity", "mi":
176189
s.addManagedIdentityProvider(1*time.Second, creds, errs)
177190
case "commandlineinterface", "cli":
@@ -180,22 +193,23 @@ func (s EnvironmentSettings) addProviderByAuthMethodName(authMethod string, cred
180193
}
181194

182195
func getAzureAuthMethods() []string {
183-
return []string{"clientcredentials", "creds", "clientcertificate", "cert", "workloadidentity", "wi", "managedidentity", "mi", "commandlineinterface", "cli", "none"}
196+
return []string{"clientcredentials", "creds", "clientcertificate", "cert", "workloadidentity", "wi", "spiffeworkloadidentity", "spiffe", "managedidentity", "mi", "commandlineinterface", "cli", "none"}
184197
}
185198

186199
// GetTokenCredential returns an azcore.TokenCredential retrieved from the order specified via
187200
// the azureAuthMethods component metadata property which denotes a comma-separated list of auth methods to try in order.
188201
// The possible values contained are (case-insensitive):
189-
// ServicePrincipal, Certificate, WorkloadIdentity, ManagedIdentity, CLI
202+
// ServicePrincipal, Certificate, WorkloadIdentity, SPIFFEWorkloadIdentity, ManagedIdentity, CLI
190203
// The string "None" can be used to disable Azure authentication.
191204
//
192205
// If the azureAuthMethods property is not present, the following order is used (which with the exception of step 5
193206
// matches the DefaultAzureCredential order):
194207
// 1. Client credentials
195208
// 2. Client certificate
196209
// 3. Workload identity
197-
// 4. MSI (we use a timeout of 1 second when no compatible managed identity implementation is available)
198-
// 5. Azure CLI
210+
// 4. SPIFFE workload identity
211+
// 5. MSI (we use a timeout of 1 second when no compatible managed identity implementation is available)
212+
// 6. Azure CLI
199213
func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error) {
200214
// Create a chain
201215
var creds []azcore.TokenCredential
@@ -212,10 +226,13 @@ func (s EnvironmentSettings) GetTokenCredential() (azcore.TokenCredential, error
212226
// 3. Workload identity
213227
s.addWorkloadIdentityProvider(&creds, &errs)
214228

215-
// 4. MSI with timeout of 1 second (same as DefaultAzureCredential)
229+
// 4. SPIFFE workload identity
230+
s.addSpiffeWorkloadIdentityProvider(&creds, &errs)
231+
232+
// 5. MSI with timeout of 1 second (same as DefaultAzureCredential)
216233
s.addManagedIdentityProvider(1*time.Second, &creds, &errs)
217234

218-
// 5. AzureCLICredential
235+
// 6. AzureCLICredential
219236
// We omit this if running in a cloud environment
220237
if !isCloudServiceWithManagedIdentity() {
221238
s.addCLIProvider(30*time.Second, &creds, &errs)
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
Copyright 2025 The Dapr Authors
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
http://www.apache.org/licenses/LICENSE-2.0
7+
Unless required by applicable law or agreed to in writing, software
8+
distributed under the License is distributed on an "AS IS" BASIS,
9+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
See the License for the specific language governing permissions and
11+
limitations under the License.
12+
*/
13+
14+
package azure
15+
16+
import (
17+
"context"
18+
"errors"
19+
"fmt"
20+
21+
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
22+
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
23+
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
24+
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
25+
26+
spiffecontext "github.com/dapr/kit/crypto/spiffe/context"
27+
)
28+
29+
const (
30+
//nolint:gosec
31+
AzureADTokenExchangeAudience = "api://AzureADTokenExchange"
32+
)
33+
34+
// SpiffeWorkloadIdentityConfig provides the options to get a bearer authorizer using SPIFFE-based workload identity.
35+
type SpiffeWorkloadIdentityConfig struct {
36+
TenantID string
37+
ClientID string
38+
AzureCloud *cloud.Configuration
39+
}
40+
41+
// GetTokenCredential returns the azcore.TokenCredential object using a SPIFFE JWT token.
42+
func (c SpiffeWorkloadIdentityConfig) GetTokenCredential() (azcore.TokenCredential, error) {
43+
if c.TenantID == "" || c.ClientID == "" {
44+
return nil, errors.New("parameters clientId and tenantId must be present for SPIFFE workload identity")
45+
}
46+
47+
var opts *azidentity.ClientAssertionCredentialOptions
48+
if c.AzureCloud != nil {
49+
opts = &azidentity.ClientAssertionCredentialOptions{
50+
ClientOptions: azcore.ClientOptions{
51+
Cloud: *c.AzureCloud,
52+
},
53+
}
54+
}
55+
56+
// Create a token provider function that retrieves the JWT from SPIFFE context
57+
tokenProvider := func(ctx context.Context) (string, error) {
58+
tknSource, ok := spiffecontext.JWTFrom(ctx)
59+
if !ok {
60+
return "", errors.New("failed to get JWT SVID source from context")
61+
}
62+
jwt, err := tknSource.FetchJWTSVID(ctx, jwtsvid.Params{
63+
Audience: AzureADTokenExchangeAudience,
64+
})
65+
if err != nil {
66+
return "", fmt.Errorf("failed to get JWT SVID: %w", err)
67+
}
68+
return jwt.Marshal(), nil
69+
}
70+
71+
return azidentity.NewClientAssertionCredential(c.TenantID, c.ClientID, tokenProvider, opts)
72+
}
73+
74+
// GetSpiffeWorkloadIdentity creates a config object from the available SPIFFE workload identity credentials.
75+
// An error is returned if required credentials are not available.
76+
func (s EnvironmentSettings) GetSpiffeWorkloadIdentity() (config SpiffeWorkloadIdentityConfig, err error) {
77+
azureCloud, err := s.GetAzureEnvironment()
78+
if err != nil {
79+
return config, err
80+
}
81+
82+
config.ClientID, _ = s.GetEnvironment("ClientID")
83+
config.TenantID, _ = s.GetEnvironment("TenantID")
84+
85+
if config.ClientID == "" || config.TenantID == "" {
86+
return config, errors.New("parameters clientId and tenantId must be present for SPIFFE workload identity")
87+
}
88+
89+
config.AzureCloud = azureCloud
90+
91+
return config, nil
92+
}

common/component/azure/blobstorage/client.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,6 @@ func (opts *ContainerClientOpts) InitContainerClient(azEnvSettings azauth.Enviro
161161
if err != nil {
162162
return nil, fmt.Errorf("cannot init blob storage container client with shared key: %w", err)
163163
}
164-
165164
// Use Azure AD as fallback
166165
default:
167166
credential, tokenErr := azEnvSettings.GetTokenCredential()

state/azure/blobstorage/v2/metadata.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,23 @@ builtinAuthenticationProfiles:
1919
sensitive: false
2020
description: "The storage account name"
2121
example: '"mystorageaccount"'
22+
- name: "azuread.spiffe"
23+
metadata:
24+
- name: accountName
25+
required: true
26+
sensitive: false
27+
description: "The storage account name"
28+
example: '"mystorageaccount"'
29+
- name: tenantId
30+
required: true
31+
sensitive: false
32+
description: "The Azure AD tenant ID"
33+
example: '"00000000-0000-0000-0000-000000000000"'
34+
- name: clientId
35+
required: true
36+
sensitive: false
37+
description: "The Azure AD client ID (application ID)"
38+
example: '"00000000-0000-0000-0000-000000000000"'
2239
authenticationProfiles:
2340
- title: "Connection string"
2441
description: "Authenticate using a connection string."

0 commit comments

Comments
 (0)