bump azure sdk

v1.3.0 of azidentity introduces support to workload identity.

Signed-off-by: Flavian Missi <fmissi@redhat.com>
This commit is contained in:
Flavian Missi
2023-05-11 15:23:47 +02:00
parent 8e29e870a4
commit 7caf058a65
169 changed files with 2892 additions and 1433 deletions

View File

@@ -1,9 +1,63 @@
# Release History
## 1.2.1 (2023-01-26)
## 1.3.0 (2023-05-09)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.3.0-beta.5
* Renamed `NewOnBehalfOfCredentialFromCertificate` to `NewOnBehalfOfCredentialWithCertificate`
* Renamed `NewOnBehalfOfCredentialFromSecret` to `NewOnBehalfOfCredentialWithSecret`
### Other Changes
* Upgrade MSAL to v0.8.1
* Upgraded to MSAL v1.0.0
## 1.3.0-beta.5 (2023-04-11)
### Breaking Changes
> These changes affect only code written against a beta version such as v1.3.0-beta.4
* Moved `NewWorkloadIdentityCredential()` parameters into `WorkloadIdentityCredentialOptions`.
The constructor now reads default configuration from environment variables set by the Azure
workload identity webhook by default.
([#20478](https://github.com/Azure/azure-sdk-for-go/pull/20478))
* Removed CAE support. It will return in v1.4.0-beta.1
([#20479](https://github.com/Azure/azure-sdk-for-go/pull/20479))
### Bugs Fixed
* Fixed an issue in `DefaultAzureCredential` that could cause the managed identity endpoint check to fail in rare circumstances.
## 1.3.0-beta.4 (2023-03-08)
### Features Added
* Added `WorkloadIdentityCredentialOptions.AdditionallyAllowedTenants` and `.DisableInstanceDiscovery`
### Bugs Fixed
* Credentials now synchronize within `GetToken()` so a single instance can be shared among goroutines
([#20044](https://github.com/Azure/azure-sdk-for-go/issues/20044))
### Other Changes
* Upgraded dependencies
## 1.2.2 (2023-03-07)
### Other Changes
* Upgraded dependencies
## 1.3.0-beta.3 (2023-02-07)
### Features Added
* By default, credentials set client capability "CP1" to enable support for
[Continuous Access Evaluation (CAE)](https://docs.microsoft.com/azure/active-directory/develop/app-resilience-continuous-access-evaluation).
This indicates to Azure Active Directory that your application can handle CAE claims challenges.
You can disable this behavior by setting the environment variable "AZURE_IDENTITY_DISABLE_CP1" to "true".
* `InteractiveBrowserCredentialOptions.LoginHint` enables pre-populating the login
prompt with a username ([#15599](https://github.com/Azure/azure-sdk-for-go/pull/15599))
* Service principal and user credentials support ADFS authentication on Azure Stack.
Specify "adfs" as the credential's tenant.
* Applications running in private or disconnected clouds can prevent credentials from
requesting Azure AD instance metadata by setting the `DisableInstanceDiscovery`
field on credential options.
* Many credentials can now be configured to authenticate in multiple tenants. The
options types for these credentials have an `AdditionallyAllowedTenants` field
that specifies additional tenants in which the credential may authenticate.
## 1.3.0-beta.2 (2023-01-10)

View File

@@ -55,8 +55,9 @@ an Azure AD access token. See [Credential Types](#credential-types "Credential T
![DefaultAzureCredential authentication flow](img/mermaidjs/DefaultAzureCredentialAuthFlow.svg)
1. **Environment** - `DefaultAzureCredential` will read account information specified via [environment variables](#environment-variables) and use it to authenticate.
2. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it.
3. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity.
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.
1. **Managed Identity** - If the app is deployed to an Azure host with managed identity enabled, `DefaultAzureCredential` will authenticate with it.
1. **Azure CLI** - If a user or service principal has authenticated via the Azure CLI `az login` command, `DefaultAzureCredential` will authenticate that identity.
> 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.
@@ -128,12 +129,13 @@ client := armresources.NewResourceGroupsClient("subscription ID", chain, nil)
|[ChainedTokenCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ChainedTokenCredential)|Define custom authentication flows, composing multiple credentials
|[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
|[ManagedIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential)|Authenticate the managed identity of an Azure resource
|[WorkloadIdentityCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#WorkloadIdentityCredential)|Authenticate a workload identity on Kubernetes
### Authenticating Service Principals
|Credential|Usage
|-|-
|[ClientAssertionCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity@v1.2.0-beta.2#ClientAssertionCredential)|Authenticate a service principal with a signed client assertion
|[ClientAssertionCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientAssertionCredential)|Authenticate a service principal with a signed client assertion
|[ClientCertificateCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientCertificateCredential)|Authenticate a service principal with a certificate
|[ClientSecretCredential](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ClientSecretCredential)|Authenticate a service principal with a secret

View File

@@ -8,16 +8,17 @@ This troubleshooting guide covers failure investigation techniques, common error
- [Permission issues](#permission-issues)
- [Find relevant information in errors](#find-relevant-information-in-errors)
- [Enable and configure logging](#enable-and-configure-logging)
- [Troubleshoot AzureCliCredential authentication issues](#troubleshoot-azureclicredential-authentication-issues)
- [Troubleshoot ClientCertificateCredential authentication issues](#troubleshoot-clientcertificatecredential-authentication-issues)
- [Troubleshoot ClientSecretCredential authentication issues](#troubleshoot-clientsecretcredential-authentication-issues)
- [Troubleshoot DefaultAzureCredential authentication issues](#troubleshoot-defaultazurecredential-authentication-issues)
- [Troubleshoot EnvironmentCredential authentication issues](#troubleshoot-environmentcredential-authentication-issues)
- [Troubleshoot ClientSecretCredential authentication issues](#troubleshoot-clientsecretcredential-authentication-issues)
- [Troubleshoot ClientCertificateCredential authentication issues](#troubleshoot-clientcertificatecredential-authentication-issues)
- [Troubleshoot UsernamePasswordCredential authentication issues](#troubleshoot-usernamepasswordcredential-authentication-issues)
- [Troubleshoot ManagedIdentityCredential authentication issues](#troubleshoot-managedidentitycredential-authentication-issues)
- [Azure Virtual Machine managed identity](#azure-virtual-machine-managed-identity)
- [Azure App Service and Azure Functions managed identity](#azure-app-service-and-azure-functions-managed-identity)
- [Azure Kubernetes Service managed identity](#azure-kubernetes-service-managed-identity)
- [Troubleshoot AzureCliCredential authentication issues](#troubleshoot-azureclicredential-authentication-issues)
- [Azure Virtual Machine managed identity](#azure-virtual-machine-managed-identity)
- [Troubleshoot UsernamePasswordCredential authentication issues](#troubleshoot-usernamepasswordcredential-authentication-issues)
- [Troubleshoot WorkloadIdentityCredential authentication issues](#troubleshoot-workloadidentitycredential-authentication-issues)
- [Get additional help](#get-additional-help)
## Handle azidentity errors
@@ -79,7 +80,7 @@ azlog.SetEvents(azidentity.EventAuthentication)
| Error |Description| Mitigation |
|---|---|---|
|"DefaultAzureCredential failed to acquire a token"|No credential in the `DefaultAzureCredential` chain provided a token|<ul><li>[Enable logging](#enable-and-configure-logging) to get further diagnostic information.</li><li>Consult the troubleshooting guide for underlying credential types for more information.</li><ul><li>[EnvironmentCredential](#troubleshoot-environmentcredential-authentication-issues)</li><li>[ManagedIdentityCredential](#troubleshoot-visualstudiocredential-authentication-issues)</li><li>[AzureCLICredential](#troubleshoot-azureclicredential-authentication-issues)</li></ul>|
|"DefaultAzureCredential failed to acquire a token"|No credential in the `DefaultAzureCredential` chain provided a token|<ul><li>[Enable logging](#enable-and-configure-logging) to get further diagnostic information.</li><li>Consult the troubleshooting guide for underlying credential types for more information.</li><ul><li>[EnvironmentCredential](#troubleshoot-environmentcredential-authentication-issues)</li><li>[ManagedIdentityCredential](#troubleshoot-managedidentitycredential-authentication-issues)</li><li>[AzureCLICredential](#troubleshoot-azureclicredential-authentication-issues)</li></ul>|
|Error from the client with a status code of 401 or 403|Authentication succeeded but the authorizing Azure service responded with a 401 (Unauthorized), or 403 (Forbidden) status code|<ul><li>[Enable logging](#enable-and-configure-logging) to determine which credential in the chain returned the authenticating token.</li><li>If an unexpected credential is returning a token, check application configuration such as environment variables.</li><li>Ensure the correct role is assigned to the authenticated identity. For example, a service specific role rather than the subscription Owner role.</li></ul>|
## Troubleshoot EnvironmentCredential authentication issues
@@ -192,6 +193,13 @@ az account get-access-token --output json --resource https://management.core.win
> This command's output will contain an access token and SHOULD NOT BE SHARED, to avoid compromising account security.
<a id="workload"></a>
## Troubleshoot `WorkloadIdentityCredential` authentication issues
| Error Message |Description| Mitigation |
|---|---|---|
|no client ID/tenant ID/token file specified|Incomplete configuration|In most cases these values are provided via environment variables set by Azure Workload Identity.<ul><li>If your application runs on Azure Kubernetes Servide (AKS) or a cluster that has deployed the Azure Workload Identity admission webhook, check pod labels and service account configuration. See the [AKS documentation](https://learn.microsoft.com/azure/aks/workload-identity-deploy-cluster#disable-workload-identity) and [Azure Workload Identity troubleshooting guide](https://azure.github.io/azure-workload-identity/docs/troubleshooting.html) for more details.<li>If your application isn't running on AKS or your cluster hasn't deployed the Workload Identity admission webhook, set these values in `WorkloadIdentityCredentialOptions`
## Get additional help
Additional information on ways to reach out for support can be found in [SUPPORT.md](https://github.com/Azure/azure-sdk-for-go/blob/main/SUPPORT.md).

View File

@@ -0,0 +1,6 @@
{
"AssetsRepo": "Azure/azure-sdk-assets",
"AssetsRepoPrefixPath": "go",
"TagPrefix": "go/azidentity",
"Tag": "go/azidentity_6225ab0470"
}

View File

@@ -15,6 +15,7 @@ import (
"net/url"
"os"
"regexp"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud"
@@ -25,15 +26,17 @@ import (
)
const (
azureAuthorityHost = "AZURE_AUTHORITY_HOST"
azureClientCertificatePassword = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
azureClientCertificatePath = "AZURE_CLIENT_CERTIFICATE_PATH"
azureClientID = "AZURE_CLIENT_ID"
azureClientSecret = "AZURE_CLIENT_SECRET"
azurePassword = "AZURE_PASSWORD"
azureRegionalAuthorityName = "AZURE_REGIONAL_AUTHORITY_NAME"
azureTenantID = "AZURE_TENANT_ID"
azureUsername = "AZURE_USERNAME"
azureAdditionallyAllowedTenants = "AZURE_ADDITIONALLY_ALLOWED_TENANTS"
azureAuthorityHost = "AZURE_AUTHORITY_HOST"
azureClientCertificatePassword = "AZURE_CLIENT_CERTIFICATE_PASSWORD"
azureClientCertificatePath = "AZURE_CLIENT_CERTIFICATE_PATH"
azureClientID = "AZURE_CLIENT_ID"
azureClientSecret = "AZURE_CLIENT_SECRET"
azureFederatedTokenFile = "AZURE_FEDERATED_TOKEN_FILE"
azurePassword = "AZURE_PASSWORD"
azureRegionalAuthorityName = "AZURE_REGIONAL_AUTHORITY_NAME"
azureTenantID = "AZURE_TENANT_ID"
azureUsername = "AZURE_USERNAME"
organizationsTenantID = "organizations"
developerSignOnClientID = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"
@@ -41,7 +44,14 @@ const (
tenantIDValidationErr = "invalid tenantID. You can locate your tenantID by following the instructions listed here: https://docs.microsoft.com/partner-center/find-ids-and-domain-names"
)
func getConfidentialClient(clientID, tenantID string, cred confidential.Credential, co *azcore.ClientOptions, additionalOpts ...confidential.Option) (confidential.Client, error) {
var (
// capability CP1 indicates the client application is capable of handling CAE claims challenges
cp1 = []string{"CP1"}
// CP1 is disabled until CAE support is added back
disableCP1 = true
)
var getConfidentialClient = func(clientID, tenantID string, cred confidential.Credential, co *azcore.ClientOptions, additionalOpts ...confidential.Option) (confidentialClient, error) {
if !validTenantID(tenantID) {
return confidential.Client{}, errors.New(tenantIDValidationErr)
}
@@ -49,16 +59,22 @@ func getConfidentialClient(clientID, tenantID string, cred confidential.Credenti
if err != nil {
return confidential.Client{}, err
}
authority := runtime.JoinPaths(authorityHost, tenantID)
o := []confidential.Option{
confidential.WithAuthority(runtime.JoinPaths(authorityHost, tenantID)),
confidential.WithAzureRegion(os.Getenv(azureRegionalAuthorityName)),
confidential.WithHTTPClient(newPipelineAdapter(co)),
}
if !disableCP1 {
o = append(o, confidential.WithClientCapabilities(cp1))
}
o = append(o, additionalOpts...)
return confidential.New(clientID, cred, o...)
if strings.ToLower(tenantID) == "adfs" {
o = append(o, confidential.WithInstanceDiscovery(false))
}
return confidential.New(authority, clientID, cred, o...)
}
func getPublicClient(clientID, tenantID string, co *azcore.ClientOptions) (public.Client, error) {
var getPublicClient = func(clientID, tenantID string, co *azcore.ClientOptions, additionalOpts ...public.Option) (public.Client, error) {
if !validTenantID(tenantID) {
return public.Client{}, errors.New(tenantIDValidationErr)
}
@@ -66,10 +82,18 @@ func getPublicClient(clientID, tenantID string, co *azcore.ClientOptions) (publi
if err != nil {
return public.Client{}, err
}
return public.New(clientID,
o := []public.Option{
public.WithAuthority(runtime.JoinPaths(authorityHost, tenantID)),
public.WithHTTPClient(newPipelineAdapter(co)),
)
}
if !disableCP1 {
o = append(o, public.WithClientCapabilities(cp1))
}
o = append(o, additionalOpts...)
if strings.ToLower(tenantID) == "adfs" {
o = append(o, public.WithInstanceDiscovery(false))
}
return public.New(clientID, o...)
}
// setAuthorityHost initializes the authority host for credentials. Precedence is:
@@ -153,6 +177,7 @@ type confidentialClient interface {
AcquireTokenSilent(ctx context.Context, scopes []string, options ...confidential.AcquireSilentOption) (confidential.AuthResult, error)
AcquireTokenByAuthCode(ctx context.Context, code string, redirectURI string, scopes []string, options ...confidential.AcquireByAuthCodeOption) (confidential.AuthResult, error)
AcquireTokenByCredential(ctx context.Context, scopes []string, options ...confidential.AcquireByCredentialOption) (confidential.AuthResult, error)
AcquireTokenOnBehalfOf(ctx context.Context, userAssertion string, scopes []string, options ...confidential.AcquireOnBehalfOfOption) (confidential.AuthResult, error)
}
// enables fakes for test scenarios

View File

@@ -23,13 +23,20 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
const credNameAzureCLI = "AzureCLICredential"
const (
credNameAzureCLI = "AzureCLICredential"
timeoutCLIRequest = 10 * time.Second
)
// used by tests to fake invoking the CLI
type azureCLITokenProvider func(ctx context.Context, resource string, tenantID string) ([]byte, error)
// AzureCLICredentialOptions contains optional parameters for AzureCLICredential.
type AzureCLICredentialOptions struct {
// AdditionallyAllowedTenants specifies tenants for which the credential may acquire tokens, in addition
// to TenantID. Add the wildcard value "*" to allow the credential to acquire tokens for any tenant the
// logged in account can access.
AdditionallyAllowedTenants []string
// TenantID identifies the tenant the credential should authenticate in.
// Defaults to the CLI's default tenant, which is typically the home tenant of the logged in user.
TenantID string
@@ -46,8 +53,8 @@ func (o *AzureCLICredentialOptions) init() {
// AzureCLICredential authenticates as the identity logged in to the Azure CLI.
type AzureCLICredential struct {
s *syncer
tokenProvider azureCLITokenProvider
tenantID string
}
// NewAzureCLICredential constructs an AzureCLICredential. Pass nil to accept default options.
@@ -57,10 +64,9 @@ func NewAzureCLICredential(options *AzureCLICredentialOptions) (*AzureCLICredent
cp = *options
}
cp.init()
return &AzureCLICredential{
tokenProvider: cp.tokenProvider,
tenantID: cp.TenantID,
}, nil
c := AzureCLICredential{tokenProvider: cp.tokenProvider}
c.s = newSyncer(credNameAzureCLI, cp.TenantID, cp.AdditionallyAllowedTenants, c.requestToken, c.requestToken)
return &c, nil
}
// GetToken requests a token from the Azure CLI. This credential doesn't cache tokens, so every call invokes the CLI.
@@ -70,24 +76,20 @@ func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequ
return azcore.AccessToken{}, errors.New(credNameAzureCLI + ": GetToken() requires exactly one scope")
}
// CLI expects an AAD v1 resource, not a v2 scope
scope := strings.TrimSuffix(opts.Scopes[0], defaultSuffix)
at, err := c.authenticate(ctx, scope)
if err != nil {
return azcore.AccessToken{}, err
}
logGetTokenSuccess(c, opts)
return at, nil
opts.Scopes = []string{strings.TrimSuffix(opts.Scopes[0], defaultSuffix)}
return c.s.GetToken(ctx, opts)
}
const timeoutCLIRequest = 10 * time.Second
func (c *AzureCLICredential) authenticate(ctx context.Context, resource string) (azcore.AccessToken, error) {
output, err := c.tokenProvider(ctx, resource, c.tenantID)
func (c *AzureCLICredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
b, err := c.tokenProvider(ctx, opts.Scopes[0], opts.TenantID)
if err != nil {
return azcore.AccessToken{}, err
}
return c.createAccessToken(output)
at, err := c.createAccessToken(b)
if err != nil {
return azcore.AccessToken{}, err
}
return at, nil
}
func defaultTokenProvider() func(ctx context.Context, resource string, tenantID string) ([]byte, error) {
@@ -100,8 +102,12 @@ func defaultTokenProvider() func(ctx context.Context, resource string, tenantID
return nil, fmt.Errorf(`%s: unexpected scope "%s". Only alphanumeric characters and ".", ";", "-", and "/" are allowed`, credNameAzureCLI, resource)
}
ctx, cancel := context.WithTimeout(ctx, timeoutCLIRequest)
defer cancel()
// set a default timeout for this authentication iff the application hasn't done so already
var cancel context.CancelFunc
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
ctx, cancel = context.WithTimeout(ctx, timeoutCLIRequest)
defer cancel()
}
commandLine := "az account get-access-token -o json --resource " + resource
if tenantID != "" {
@@ -158,32 +164,17 @@ func (c *AzureCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, e
return azcore.AccessToken{}, err
}
tokenExpirationDate, err := parseExpirationDate(t.ExpiresOn)
// the Azure CLI's "expiresOn" is local time
exp, err := time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local)
if err != nil {
return azcore.AccessToken{}, fmt.Errorf("Error parsing Token Expiration Date %q: %+v", t.ExpiresOn, err)
return azcore.AccessToken{}, fmt.Errorf("Error parsing token expiration time %q: %v", t.ExpiresOn, err)
}
converted := azcore.AccessToken{
Token: t.AccessToken,
ExpiresOn: *tokenExpirationDate,
ExpiresOn: exp.UTC(),
}
return converted, nil
}
// parseExpirationDate parses either a Azure CLI or CloudShell date into a time object
func parseExpirationDate(input string) (*time.Time, error) {
// CloudShell (and potentially the Azure CLI in future)
expirationDate, cloudShellErr := time.Parse(time.RFC3339, input)
if cloudShellErr != nil {
// Azure CLI (Python) e.g. 2017-08-31 19:48:57.998857 (plus the local timezone)
const cliFormat = "2006-01-02 15:04:05.999999"
expirationDate, cliErr := time.ParseInLocation(cliFormat, input, time.Local)
if cliErr != nil {
return nil, fmt.Errorf("Error parsing expiration date %q.\n\nCloudShell Error: \n%+v\n\nCLI Error:\n%+v", input, cloudShellErr, cliErr)
}
return &expirationDate, nil
}
return &expirationDate, nil
}
var _ azcore.TokenCredential = (*AzureCLICredential)(nil)

View File

@@ -81,10 +81,13 @@ func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.Token
}
}
var err error
var errs []error
var token azcore.AccessToken
var successfulCredential azcore.TokenCredential
var (
err error
errs []error
successfulCredential azcore.TokenCredential
token azcore.AccessToken
unavailableErr *credentialUnavailableError
)
for _, cred := range c.sources {
token, err = cred.GetToken(ctx, opts)
if err == nil {
@@ -93,12 +96,14 @@ func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.Token
break
}
errs = append(errs, err)
if _, ok := err.(*credentialUnavailableError); !ok {
// continue to the next source iff this one returned credentialUnavailableError
if !errors.As(err, &unavailableErr) {
break
}
}
if c.iterating {
c.cond.L.Lock()
// this is nil when all credentials returned an error
c.successfulCredential = successfulCredential
c.iterating = false
c.cond.L.Unlock()
@@ -108,11 +113,11 @@ func (c *ChainedTokenCredential) GetToken(ctx context.Context, opts policy.Token
if err != nil {
// return credentialUnavailableError iff all sources did so; return AuthenticationFailedError otherwise
msg := createChainedErrorMessage(errs)
if _, ok := err.(*credentialUnavailableError); ok {
if errors.As(err, &unavailableErr) {
err = newCredentialUnavailableError(c.name, msg)
} else {
res := getResponseFromError(err)
err = newAuthenticationFailedError(c.name, msg, res)
err = newAuthenticationFailedError(c.name, msg, res, err)
}
}
return token, err

View File

@@ -18,18 +18,29 @@ import (
const credNameAssertion = "ClientAssertionCredential"
// ClientAssertionCredential authenticates an application with assertions provided by a callback function.
// This credential is for advanced scenarios. ClientCertificateCredential has a more convenient API for
// This credential is for advanced scenarios. [ClientCertificateCredential] has a more convenient API for
// the most common assertion scenario, authenticating a service principal with a certificate. See
// [Azure AD documentation] for details of the assertion format.
//
// [Azure AD documentation]: https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#assertion-format
type ClientAssertionCredential struct {
client confidentialClient
s *syncer
}
// ClientAssertionCredentialOptions contains optional parameters for ClientAssertionCredential.
type ClientAssertionCredentialOptions 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 should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// NewClientAssertionCredential constructs a ClientAssertionCredential. The getAssertion function must be thread safe. Pass nil for options to accept defaults.
@@ -45,29 +56,27 @@ func NewClientAssertionCredential(tenantID, clientID string, getAssertion func(c
return getAssertion(ctx)
},
)
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions)
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
if err != nil {
return nil, err
}
return &ClientAssertionCredential{client: c}, nil
cac := ClientAssertionCredential{client: c}
cac.s = newSyncer(credNameAssertion, tenantID, options.AdditionallyAllowedTenants, cac.requestToken, cac.silentAuth)
return &cac, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *ClientAssertionCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) == 0 {
return azcore.AccessToken{}, errors.New(credNameAssertion + ": GetToken() requires at least one scope")
}
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
if err == nil {
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
return c.s.GetToken(ctx, opts)
}
ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes)
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameAssertion, err)
}
logGetTokenSuccess(c, opts)
func (c *ClientAssertionCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ClientAssertionCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}

View File

@@ -25,6 +25,15 @@ const credNameCert = "ClientCertificateCredential"
type ClientCertificateCredentialOptions 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 should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// SendCertificateChain controls whether the credential sends the public certificate chain in the x5c
// header of each token request's JWT. This is required for Subject Name/Issuer (SNI) authentication.
// Defaults to False.
@@ -34,6 +43,7 @@ type ClientCertificateCredentialOptions struct {
// ClientCertificateCredential authenticates a service principal with a certificate.
type ClientCertificateCredential struct {
client confidentialClient
s *syncer
}
// NewClientCertificateCredential constructs a ClientCertificateCredential. Pass nil for options to accept defaults.
@@ -44,7 +54,7 @@ func NewClientCertificateCredential(tenantID string, clientID string, certs []*x
if options == nil {
options = &ClientCertificateCredentialOptions{}
}
cred, err := confidential.NewCredFromCertChain(certs, key)
cred, err := confidential.NewCredFromCert(certs, key)
if err != nil {
return nil, err
}
@@ -52,29 +62,28 @@ func NewClientCertificateCredential(tenantID string, clientID string, certs []*x
if options.SendCertificateChain {
o = append(o, confidential.WithX5C())
}
o = append(o, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions, o...)
if err != nil {
return nil, err
}
return &ClientCertificateCredential{client: c}, nil
cc := ClientCertificateCredential{client: c}
cc.s = newSyncer(credNameCert, tenantID, options.AdditionallyAllowedTenants, cc.requestToken, cc.silentAuth)
return &cc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *ClientCertificateCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) == 0 {
return azcore.AccessToken{}, errors.New(credNameCert + ": GetToken() requires at least one scope")
}
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
if err == nil {
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
return c.s.GetToken(ctx, opts)
}
ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes)
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameCert, err)
}
logGetTokenSuccess(c, opts)
func (c *ClientCertificateCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ClientCertificateCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}

View File

@@ -8,7 +8,6 @@ package azidentity
import (
"context"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
@@ -20,11 +19,22 @@ const credNameSecret = "ClientSecretCredential"
// ClientSecretCredentialOptions contains optional parameters for ClientSecretCredential.
type ClientSecretCredentialOptions 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 should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// ClientSecretCredential authenticates an application with a client secret.
type ClientSecretCredential struct {
client confidentialClient
s *syncer
}
// NewClientSecretCredential constructs a ClientSecretCredential. Pass nil for options to accept defaults.
@@ -36,29 +46,29 @@ func NewClientSecretCredential(tenantID string, clientID string, clientSecret st
if err != nil {
return nil, err
}
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions)
c, err := getConfidentialClient(
clientID, tenantID, cred, &options.ClientOptions, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery),
)
if err != nil {
return nil, err
}
return &ClientSecretCredential{client: c}, nil
csc := ClientSecretCredential{client: c}
csc.s = newSyncer(credNameSecret, tenantID, options.AdditionallyAllowedTenants, csc.requestToken, csc.silentAuth)
return &csc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *ClientSecretCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) == 0 {
return azcore.AccessToken{}, errors.New(credNameSecret + ": GetToken() requires at least one scope")
}
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
if err == nil {
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
return c.s.GetToken(ctx, opts)
}
ar, err = c.client.AcquireTokenByCredential(ctx, opts.Scopes)
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameSecret, err)
}
logGetTokenSuccess(c, opts)
func (c *ClientSecretCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ClientSecretCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}

View File

@@ -23,6 +23,15 @@ import (
type DefaultAzureCredentialOptions 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. This value can also be
// set as a semicolon delimited list of tenants in the environment variable AZURE_ADDITIONALLY_ALLOWED_TENANTS.
AdditionallyAllowedTenants []string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID identifies the tenant the Azure CLI should authenticate in.
// Defaults to the CLI's default tenant, which is typically the home tenant of the user logged in to the CLI.
TenantID string
@@ -30,11 +39,15 @@ type DefaultAzureCredentialOptions struct {
// DefaultAzureCredential is a default credential chain for applications that will deploy to Azure.
// It combines credentials suitable for deployment with credentials suitable for local development.
// It attempts to authenticate with each of these credential types, in the following order, stopping when one provides a token:
// It attempts to authenticate with each of these credential types, in the following order, stopping
// when one provides a token:
//
// EnvironmentCredential
// ManagedIdentityCredential
// AzureCLICredential
// - [EnvironmentCredential]
// - [WorkloadIdentityCredential], if environment variable configuration is set by the Azure workload
// identity webhook. Use [WorkloadIdentityCredential] directly when not using the webhook or needing
// more control over its configuration.
// - [ManagedIdentityCredential]
// - [AzureCLICredential]
//
// Consult the documentation for these credential types for more information on how they authenticate.
// Once a credential has successfully authenticated, DefaultAzureCredential will use that credential for
@@ -51,8 +64,18 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default
if options == nil {
options = &DefaultAzureCredentialOptions{}
}
additionalTenants := options.AdditionallyAllowedTenants
if len(additionalTenants) == 0 {
if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
additionalTenants = strings.Split(tenants, ";")
}
}
envCred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{ClientOptions: options.ClientOptions})
envCred, err := NewEnvironmentCredential(&EnvironmentCredentialOptions{
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
additionallyAllowedTenants: additionalTenants,
})
if err == nil {
creds = append(creds, envCred)
} else {
@@ -60,20 +83,31 @@ func NewDefaultAzureCredential(options *DefaultAzureCredentialOptions) (*Default
creds = append(creds, &defaultCredentialErrorReporter{credType: "EnvironmentCredential", err: err})
}
// workload identity requires values for AZURE_AUTHORITY_HOST, AZURE_CLIENT_ID, AZURE_FEDERATED_TOKEN_FILE, AZURE_TENANT_ID
wic, err := NewWorkloadIdentityCredential(&WorkloadIdentityCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
})
if err == nil {
creds = append(creds, wic)
} else {
errorMessages = append(errorMessages, credNameWorkloadIdentity+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameWorkloadIdentity, err: err})
}
o := &ManagedIdentityCredentialOptions{ClientOptions: options.ClientOptions}
if ID, ok := os.LookupEnv(azureClientID); ok {
o.ID = ClientID(ID)
}
msiCred, err := NewManagedIdentityCredential(o)
miCred, err := NewManagedIdentityCredential(o)
if err == nil {
creds = append(creds, msiCred)
msiCred.mic.imdsTimeout = time.Second
creds = append(creds, &timeoutWrapper{mic: miCred, timeout: time.Second})
} else {
errorMessages = append(errorMessages, credNameManagedIdentity+": "+err.Error())
creds = append(creds, &defaultCredentialErrorReporter{credType: credNameManagedIdentity, err: err})
}
cliCred, err := NewAzureCLICredential(&AzureCLICredentialOptions{TenantID: options.TenantID})
cliCred, err := NewAzureCLICredential(&AzureCLICredentialOptions{AdditionallyAllowedTenants: additionalTenants, TenantID: options.TenantID})
if err == nil {
creds = append(creds, cliCred)
} else {
@@ -132,3 +166,44 @@ func (d *defaultCredentialErrorReporter) GetToken(ctx context.Context, opts poli
}
var _ azcore.TokenCredential = (*defaultCredentialErrorReporter)(nil)
// timeoutWrapper prevents a potentially very long timeout when managed identity isn't available
type timeoutWrapper struct {
mic *ManagedIdentityCredential
// timeout applies to all auth attempts until one doesn't time out
timeout time.Duration
}
// GetToken wraps DefaultAzureCredential's initial managed identity auth attempt with a short timeout
// because managed identity may not be available and connecting to IMDS can take several minutes to time out.
func (w *timeoutWrapper) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var tk azcore.AccessToken
var err error
// no need to synchronize around this value because it's written only within ChainedTokenCredential's critical section
if w.timeout > 0 {
c, cancel := context.WithTimeout(ctx, w.timeout)
defer cancel()
tk, err = w.mic.GetToken(c, opts)
if isAuthFailedDueToContext(err) {
err = newCredentialUnavailableError(credNameManagedIdentity, "managed identity timed out")
} else {
// some managed identity implementation is available, so don't apply the timeout to future calls
w.timeout = 0
}
} else {
tk, err = w.mic.GetToken(ctx, opts)
}
return tk, err
}
// unwraps nested AuthenticationFailedErrors to get the root error
func isAuthFailedDueToContext(err error) bool {
for {
var authFailedErr *AuthenticationFailedError
if !errors.As(err, &authFailedErr) {
break
}
err = authFailedErr.err
}
return errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
}

View File

@@ -8,7 +8,6 @@ package azidentity
import (
"context"
"errors"
"fmt"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
@@ -22,13 +21,22 @@ const credNameDeviceCode = "DeviceCodeCredential"
type DeviceCodeCredentialOptions 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.
AdditionallyAllowedTenants []string
// ClientID is the ID of the application users will authenticate to.
// Defaults to the ID of an Azure development application.
ClientID string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID is the Azure Active Directory tenant the credential authenticates in. Defaults to the
// "organizations" tenant, which can authenticate work and school accounts. Required for single-tenant
// applications.
TenantID string
// ClientID is the ID of the application users will authenticate to.
// Defaults to the ID of an Azure development application.
ClientID string
// UserPrompt controls how the credential presents authentication instructions. The credential calls
// this function with authentication details when it receives a device code. By default, the credential
// prints these details to stdout.
@@ -66,9 +74,10 @@ type DeviceCodeMessage struct {
// If a web browser is available, InteractiveBrowserCredential is more convenient because it
// automatically opens a browser to the login page.
type DeviceCodeCredential struct {
client publicClient
userPrompt func(context.Context, DeviceCodeMessage) error
account public.Account
account public.Account
client publicClient
s *syncer
prompt func(context.Context, DeviceCodeMessage) error
}
// NewDeviceCodeCredential creates a DeviceCodeCredential. Pass nil to accept default options.
@@ -78,41 +87,49 @@ func NewDeviceCodeCredential(options *DeviceCodeCredentialOptions) (*DeviceCodeC
cp = *options
}
cp.init()
c, err := getPublicClient(cp.ClientID, cp.TenantID, &cp.ClientOptions)
c, err := getPublicClient(
cp.ClientID, cp.TenantID, &cp.ClientOptions, public.WithInstanceDiscovery(!cp.DisableInstanceDiscovery),
)
if err != nil {
return nil, err
}
return &DeviceCodeCredential{userPrompt: cp.UserPrompt, client: c}, nil
cred := DeviceCodeCredential{client: c, prompt: cp.UserPrompt}
cred.s = newSyncer(credNameDeviceCode, cp.TenantID, cp.AdditionallyAllowedTenants, cred.requestToken, cred.silentAuth)
return &cred, nil
}
// GetToken requests an access token from Azure Active Directory. It will begin the device code flow and poll until the user completes authentication.
// This method is called automatically by Azure SDK clients.
func (c *DeviceCodeCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) == 0 {
return azcore.AccessToken{}, errors.New(credNameDeviceCode + ": GetToken() requires at least one scope")
}
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account))
if err == nil {
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
dc, err := c.client.AcquireTokenByDeviceCode(ctx, opts.Scopes)
return c.s.GetToken(ctx, opts)
}
func (c *DeviceCodeCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
dc, err := c.client.AcquireTokenByDeviceCode(ctx, opts.Scopes, public.WithTenantID(opts.TenantID))
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameDeviceCode, err)
return azcore.AccessToken{}, err
}
err = c.userPrompt(ctx, DeviceCodeMessage{
err = c.prompt(ctx, DeviceCodeMessage{
Message: dc.Result.Message,
UserCode: dc.Result.UserCode,
VerificationURL: dc.Result.VerificationURL,
Message: dc.Result.Message,
})
if err != nil {
return azcore.AccessToken{}, err
}
ar, err = dc.AuthenticationResult(ctx)
ar, err := dc.AuthenticationResult(ctx)
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameDeviceCode, err)
return azcore.AccessToken{}, err
}
c.account = ar.Account
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *DeviceCodeCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes,
public.WithSilentAccount(c.account),
public.WithTenantID(opts.TenantID),
)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}

View File

@@ -23,6 +23,17 @@ const envVarSendCertChain = "AZURE_CLIENT_SEND_CERTIFICATE_CHAIN"
// EnvironmentCredentialOptions contains optional parameters for EnvironmentCredential
type EnvironmentCredentialOptions struct {
azcore.ClientOptions
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// additionallyAllowedTenants is used only by NewDefaultAzureCredential() to enable that constructor's explicit
// option to override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS. Applications using EnvironmentCredential
// directly should set that variable instead. This field should remain unexported to preserve this credential's
// unambiguous "all configuration from environment variables" design.
additionallyAllowedTenants []string
}
// EnvironmentCredential authenticates a service principal with a secret or certificate, or a user with a password, depending
@@ -55,6 +66,12 @@ type EnvironmentCredentialOptions struct {
// AZURE_USERNAME: a username (usually an email address)
//
// AZURE_PASSWORD: the user's password
//
// # Configuration for multitenant applications
//
// To enable multitenant authentication, set AZURE_ADDITIONALLY_ALLOWED_TENANTS with a semicolon delimited list of tenants
// the credential may request tokens from in addition to the tenant specified by AZURE_TENANT_ID. Set
// AZURE_ADDITIONALLY_ALLOWED_TENANTS to "*" to enable the credential to request a token from any tenant.
type EnvironmentCredential struct {
cred azcore.TokenCredential
}
@@ -72,9 +89,20 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
if clientID == "" {
return nil, errors.New("missing environment variable " + azureClientID)
}
// tenants set by NewDefaultAzureCredential() override the value of AZURE_ADDITIONALLY_ALLOWED_TENANTS
additionalTenants := options.additionallyAllowedTenants
if len(additionalTenants) == 0 {
if tenants := os.Getenv(azureAdditionallyAllowedTenants); tenants != "" {
additionalTenants = strings.Split(tenants, ";")
}
}
if clientSecret := os.Getenv(azureClientSecret); clientSecret != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with ClientSecretCredential")
o := &ClientSecretCredentialOptions{ClientOptions: options.ClientOptions}
o := &ClientSecretCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewClientSecretCredential(tenantID, clientID, clientSecret, o)
if err != nil {
return nil, err
@@ -95,7 +123,11 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
if err != nil {
return nil, fmt.Errorf(`failed to load certificate from "%s": %v`, certPath, err)
}
o := &ClientCertificateCredentialOptions{ClientOptions: options.ClientOptions}
o := &ClientCertificateCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
if v, ok := os.LookupEnv(envVarSendCertChain); ok {
o.SendCertificateChain = v == "1" || strings.ToLower(v) == "true"
}
@@ -108,7 +140,11 @@ func NewEnvironmentCredential(options *EnvironmentCredentialOptions) (*Environme
if username := os.Getenv(azureUsername); username != "" {
if password := os.Getenv(azurePassword); password != "" {
log.Write(EventAuthentication, "EnvironmentCredential will authenticate with UsernamePasswordCredential")
o := &UsernamePasswordCredentialOptions{ClientOptions: options.ClientOptions}
o := &UsernamePasswordCredentialOptions{
AdditionallyAllowedTenants: additionalTenants,
ClientOptions: options.ClientOptions,
DisableInstanceDiscovery: options.DisableInstanceDiscovery,
}
cred, err := NewUsernamePasswordCredential(tenantID, clientID, username, password, o)
if err != nil {
return nil, err

View File

@@ -39,15 +39,11 @@ type AuthenticationFailedError struct {
credType string
message string
err error
}
func newAuthenticationFailedError(credType string, message string, resp *http.Response) error {
return &AuthenticationFailedError{credType: credType, message: message, RawResponse: resp}
}
func newAuthenticationFailedErrorFromMSALError(credType string, err error) error {
res := getResponseFromError(err)
return newAuthenticationFailedError(credType, err.Error(), res)
func newAuthenticationFailedError(credType string, message string, resp *http.Response, err error) error {
return &AuthenticationFailedError{credType: credType, message: message, RawResponse: resp, err: err}
}
// Error implements the error interface. Note that the message contents are not contractual and can change over time.
@@ -87,6 +83,8 @@ func (e *AuthenticationFailedError) Error() string {
anchor = "managed-id"
case credNameUserPassword:
anchor = "username-password"
case credNameWorkloadIdentity:
anchor = "workload"
}
if anchor != "" {
fmt.Fprintf(msg, "To troubleshoot, visit https://aka.ms/azsdk/go/identity/troubleshoot#%s", anchor)
@@ -101,24 +99,31 @@ func (*AuthenticationFailedError) NonRetriable() {
var _ errorinfo.NonRetriable = (*AuthenticationFailedError)(nil)
// credentialUnavailableError indicates a credential can't attempt
// authentication because it lacks required data or state.
// credentialUnavailableError indicates a credential can't attempt authentication because it lacks required
// data or state
type credentialUnavailableError struct {
credType string
message string
message string
}
// newCredentialUnavailableError is an internal helper that ensures consistent error message formatting
func newCredentialUnavailableError(credType, message string) error {
return &credentialUnavailableError{credType: credType, message: message}
msg := fmt.Sprintf("%s: %s", credType, message)
return &credentialUnavailableError{msg}
}
// NewCredentialUnavailableError constructs an error indicating a credential can't attempt authentication
// because it lacks required data or state. When [ChainedTokenCredential] receives this error it will try
// its next credential, if any.
func NewCredentialUnavailableError(message string) error {
return &credentialUnavailableError{message}
}
// Error implements the error interface. Note that the message contents are not contractual and can change over time.
func (e *credentialUnavailableError) Error() string {
return e.credType + ": " + e.message
return e.message
}
// NonRetriable indicates that this error should not be retried.
func (e *credentialUnavailableError) NonRetriable() {
// marker method
}
// NonRetriable is a marker method indicating this error should not be retried. It has no implementation.
func (e *credentialUnavailableError) NonRetriable() {}
var _ errorinfo.NonRetriable = (*credentialUnavailableError)(nil)

View File

@@ -8,28 +8,41 @@ package azidentity
import (
"context"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/public"
)
const credNameBrowser = "InteractiveBrowserCredentiall"
const credNameBrowser = "InteractiveBrowserCredential"
// InteractiveBrowserCredentialOptions contains optional parameters for InteractiveBrowserCredential.
type InteractiveBrowserCredentialOptions struct {
azcore.ClientOptions
// TenantID is the Azure Active Directory tenant the credential authenticates in. Defaults to the
// "organizations" tenant, which can authenticate work and school accounts.
TenantID string
// 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.
AdditionallyAllowedTenants []string
// ClientID is the ID of the application users will authenticate to.
// Defaults to the ID of an Azure development application.
ClientID string
// RedirectURL will be supported in a future version but presently doesn't work: https://github.com/Azure/azure-sdk-for-go/issues/15632.
// Applications which have "http://localhost" registered as a redirect URL need not set this option.
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// LoginHint pre-populates the account prompt with a username. Users may choose to authenticate a different account.
LoginHint string
// RedirectURL is the URL Azure Active Directory will redirect to with the access token. This is required
// only when setting ClientID, and must match a redirect URI in the application's registration.
// Applications which have registered "http://localhost" as a redirect URI need not set this option.
RedirectURL string
// TenantID is the Azure Active Directory tenant the credential authenticates in. Defaults to the
// "organizations" tenant, which can authenticate work and school accounts.
TenantID string
}
func (o *InteractiveBrowserCredentialOptions) init() {
@@ -43,9 +56,10 @@ func (o *InteractiveBrowserCredentialOptions) init() {
// InteractiveBrowserCredential opens a browser to interactively authenticate a user.
type InteractiveBrowserCredential struct {
account public.Account
client publicClient
options InteractiveBrowserCredentialOptions
account public.Account
s *syncer
}
// NewInteractiveBrowserCredential constructs a new InteractiveBrowserCredential. Pass nil to accept default options.
@@ -55,30 +69,37 @@ func NewInteractiveBrowserCredential(options *InteractiveBrowserCredentialOption
cp = *options
}
cp.init()
c, err := getPublicClient(cp.ClientID, cp.TenantID, &cp.ClientOptions)
c, err := getPublicClient(cp.ClientID, cp.TenantID, &cp.ClientOptions, public.WithInstanceDiscovery(!cp.DisableInstanceDiscovery))
if err != nil {
return nil, err
}
return &InteractiveBrowserCredential{options: cp, client: c}, nil
ibc := InteractiveBrowserCredential{client: c, options: cp}
ibc.s = newSyncer(credNameBrowser, cp.TenantID, cp.AdditionallyAllowedTenants, ibc.requestToken, ibc.silentAuth)
return &ibc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *InteractiveBrowserCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) == 0 {
return azcore.AccessToken{}, errors.New(credNameBrowser + ": GetToken() requires at least one scope")
}
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account))
if err == nil {
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
return c.s.GetToken(ctx, opts)
}
ar, err = c.client.AcquireTokenInteractive(ctx, opts.Scopes, public.WithRedirectURI(c.options.RedirectURL))
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameBrowser, err)
func (c *InteractiveBrowserCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenInteractive(ctx, opts.Scopes,
public.WithLoginHint(c.options.LoginHint),
public.WithRedirectURI(c.options.RedirectURL),
public.WithTenantID(opts.TenantID),
)
if err == nil {
c.account = ar.Account
}
c.account = ar.Account
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *InteractiveBrowserCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes,
public.WithSilentAccount(c.account),
public.WithTenantID(opts.TenantID),
)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}

View File

@@ -6,25 +6,9 @@
package azidentity
import (
"fmt"
"strings"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
import "github.com/Azure/azure-sdk-for-go/sdk/internal/log"
// EventAuthentication entries contain information about authentication.
// This includes information like the names of environment variables
// used when obtaining credentials and the type of credential used.
const EventAuthentication log.Event = "Authentication"
func logGetTokenSuccess(cred azcore.TokenCredential, opts policy.TokenRequestOptions) {
if !log.Should(EventAuthentication) {
return
}
scope := strings.Join(opts.Scopes, ", ")
msg := fmt.Sprintf("%T.GetToken() acquired a token for scope %s\n", cred, scope)
log.Write(EventAuthentication, msg)
}

View File

@@ -55,11 +55,10 @@ const (
// managedIdentityClient provides the base for authenticating in managed identity environments
// This type includes an runtime.Pipeline and TokenCredentialOptions.
type managedIdentityClient struct {
pipeline runtime.Pipeline
msiType msiType
endpoint string
id ManagedIDKind
imdsTimeout time.Duration
pipeline runtime.Pipeline
msiType msiType
endpoint string
id ManagedIDKind
}
type wrappedNumber json.Number
@@ -162,12 +161,6 @@ func (c *managedIdentityClient) provideToken(ctx context.Context, params confide
// authenticate acquires an access token
func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKind, scopes []string) (azcore.AccessToken, error) {
var cancel context.CancelFunc
if c.imdsTimeout > 0 && c.msiType == msiTypeIMDS {
ctx, cancel = context.WithTimeout(ctx, c.imdsTimeout)
defer cancel()
}
msg, err := c.createAuthRequest(ctx, id, scopes)
if err != nil {
return azcore.AccessToken{}, err
@@ -175,27 +168,21 @@ func (c *managedIdentityClient) authenticate(ctx context.Context, id ManagedIDKi
resp, err := c.pipeline.Do(msg)
if err != nil {
if cancel != nil && errors.Is(err, context.DeadlineExceeded) {
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, "IMDS token request timed out")
}
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, err.Error(), nil)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, err.Error(), nil, err)
}
// got a response, remove the IMDS timeout so future requests use the transport's configuration
c.imdsTimeout = 0
if runtime.HasStatusCode(resp, http.StatusOK, http.StatusCreated) {
return c.createAccessToken(resp)
}
if c.msiType == msiTypeIMDS && resp.StatusCode == 400 {
if id != nil {
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "the requested identity isn't assigned to this resource", resp)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "the requested identity isn't assigned to this resource", resp, nil)
}
return azcore.AccessToken{}, newCredentialUnavailableError(credNameManagedIdentity, "no default identity is assigned to this resource")
}
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "authentication failed", resp)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "authentication failed", resp, nil)
}
func (c *managedIdentityClient) createAccessToken(res *http.Response) (azcore.AccessToken, error) {
@@ -223,10 +210,10 @@ func (c *managedIdentityClient) createAccessToken(res *http.Response) (azcore.Ac
if expiresOn, err := strconv.Atoi(v); err == nil {
return azcore.AccessToken{Token: value.Token, ExpiresOn: time.Unix(int64(expiresOn), 0).UTC()}, nil
}
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "unexpected expires_on value: "+v, res)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, "unexpected expires_on value: "+v, res, nil)
default:
msg := fmt.Sprintf("unsupported type received in expires_on: %T, %v", v, v)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, msg, res)
return azcore.AccessToken{}, newAuthenticationFailedError(credNameManagedIdentity, msg, res, nil)
}
}
@@ -241,7 +228,7 @@ func (c *managedIdentityClient) createAuthRequest(ctx context.Context, id Manage
key, err := c.getAzureArcSecretKey(ctx, scopes)
if err != nil {
msg := fmt.Sprintf("failed to retreive secret key from the identity endpoint: %v", err)
return nil, newAuthenticationFailedError(credNameManagedIdentity, msg, nil)
return nil, newAuthenticationFailedError(credNameManagedIdentity, msg, nil, err)
}
return c.createAzureArcAuthRequest(ctx, id, scopes, key)
case msiTypeServiceFabric:
@@ -335,7 +322,7 @@ func (c *managedIdentityClient) getAzureArcSecretKey(ctx context.Context, resour
// of the secret key file. Any other status code indicates an error in the request.
if response.StatusCode != 401 {
msg := fmt.Sprintf("expected a 401 response, received %d", response.StatusCode)
return "", newAuthenticationFailedError(credNameManagedIdentity, msg, response)
return "", newAuthenticationFailedError(credNameManagedIdentity, msg, response, nil)
}
header := response.Header.Get("WWW-Authenticate")
if len(header) == 0 {

View File

@@ -73,6 +73,7 @@ type ManagedIdentityCredentialOptions struct {
type ManagedIdentityCredential struct {
client confidentialClient
mic *managedIdentityClient
s *syncer
}
// NewManagedIdentityCredential creates a ManagedIdentityCredential. Pass nil to accept default options.
@@ -85,20 +86,21 @@ func NewManagedIdentityCredential(options *ManagedIdentityCredentialOptions) (*M
return nil, err
}
cred := confidential.NewCredFromTokenProvider(mic.provideToken)
if err != nil {
return nil, err
}
// It's okay to give MSAL an invalid client ID because MSAL will use it only as part of a cache key.
// ManagedIdentityClient handles all the details of authentication and won't receive this value from MSAL.
clientID := "SYSTEM-ASSIGNED-MANAGED-IDENTITY"
if options.ID != nil {
clientID = options.ID.String()
}
c, err := confidential.New(clientID, cred)
// similarly, it's okay to give MSAL an incorrect authority URL because that URL won't be used
c, err := confidential.New("https://login.microsoftonline.com/common", clientID, cred)
if err != nil {
return nil, err
}
return &ManagedIdentityCredential{client: c, mic: mic}, nil
m := ManagedIdentityCredential{client: c, mic: mic}
m.s = newSyncer(credNameManagedIdentity, "", nil, m.requestToken, m.silentAuth)
return &m, nil
}
// GetToken requests an access token from the hosting environment. This method is called automatically by Azure SDK clients.
@@ -108,17 +110,17 @@ func (c *ManagedIdentityCredential) GetToken(ctx context.Context, opts policy.To
return azcore.AccessToken{}, err
}
// managed identity endpoints require an AADv1 resource (i.e. token audience), not a v2 scope, so we remove "/.default" here
scopes := []string{strings.TrimSuffix(opts.Scopes[0], defaultSuffix)}
ar, err := c.client.AcquireTokenSilent(ctx, scopes)
if err == nil {
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, nil
}
ar, err = c.client.AcquireTokenByCredential(ctx, scopes)
if err != nil {
return azcore.AccessToken{}, err
}
logGetTokenSuccess(c, opts)
opts.Scopes = []string{strings.TrimSuffix(opts.Scopes[0], defaultSuffix)}
return c.s.GetToken(ctx, opts)
}
func (c *ManagedIdentityCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByCredential(ctx, opts.Scopes)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *ManagedIdentityCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}

View File

@@ -0,0 +1,99 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"crypto"
"crypto/x509"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)
const credNameOBO = "OnBehalfOfCredential"
// OnBehalfOfCredential authenticates a service principal via the on-behalf-of flow. This is typically used by
// middle-tier services that authorize requests to other services with a delegated user identity. Because this
// is not an interactive authentication flow, an application using it must have admin consent for any delegated
// permissions before requesting tokens for them. See [Azure Active Directory documentation] for more details.
//
// [Azure Active Directory documentation]: https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow
type OnBehalfOfCredential struct {
assertion string
client confidentialClient
s *syncer
}
// OnBehalfOfCredentialOptions contains optional parameters for OnBehalfOfCredential
type OnBehalfOfCredentialOptions 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 should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// SendCertificateChain applies only when the credential is configured to authenticate with a certificate.
// This setting controls whether the credential sends the public certificate chain in the x5c header of each
// token request's JWT. This is required for, and only used in, Subject Name/Issuer (SNI) authentication.
SendCertificateChain bool
}
// NewOnBehalfOfCredentialWithCertificate constructs an OnBehalfOfCredential that authenticates with a certificate.
// See [ParseCertificates] for help loading a certificate.
func NewOnBehalfOfCredentialWithCertificate(tenantID, clientID, userAssertion string, certs []*x509.Certificate, key crypto.PrivateKey, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
cred, err := confidential.NewCredFromCert(certs, key)
if err != nil {
return nil, err
}
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
// NewOnBehalfOfCredentialWithSecret constructs an OnBehalfOfCredential that authenticates with a client secret.
func NewOnBehalfOfCredentialWithSecret(tenantID, clientID, userAssertion, clientSecret string, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
cred, err := confidential.NewCredFromSecret(clientSecret)
if err != nil {
return nil, err
}
return newOnBehalfOfCredential(tenantID, clientID, userAssertion, cred, options)
}
func newOnBehalfOfCredential(tenantID, clientID, userAssertion string, cred confidential.Credential, options *OnBehalfOfCredentialOptions) (*OnBehalfOfCredential, error) {
if options == nil {
options = &OnBehalfOfCredentialOptions{}
}
opts := []confidential.Option{}
if options.SendCertificateChain {
opts = append(opts, confidential.WithX5C())
}
opts = append(opts, confidential.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
c, err := getConfidentialClient(clientID, tenantID, cred, &options.ClientOptions, opts...)
if err != nil {
return nil, err
}
obo := OnBehalfOfCredential{assertion: userAssertion, client: c}
obo.s = newSyncer(credNameOBO, tenantID, options.AdditionallyAllowedTenants, obo.requestToken, obo.requestToken)
return &obo, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (o *OnBehalfOfCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return o.s.GetToken(ctx, opts)
}
func (o *OnBehalfOfCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := o.client.AcquireTokenOnBehalfOf(ctx, o.assertion, opts.Scopes, confidential.WithTenantID(opts.TenantID))
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
var _ azcore.TokenCredential = (*OnBehalfOfCredential)(nil)

View File

@@ -0,0 +1,130 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
)
type authFn func(context.Context, policy.TokenRequestOptions) (azcore.AccessToken, error)
// syncer synchronizes authentication calls so that goroutines can share a credential instance
type syncer struct {
addlTenants []string
authing bool
cond *sync.Cond
reqToken, silent authFn
name, tenant string
}
func newSyncer(name, tenant string, additionalTenants []string, reqToken, silentAuth authFn) *syncer {
return &syncer{
addlTenants: resolveAdditionalTenants(additionalTenants),
cond: &sync.Cond{L: &sync.Mutex{}},
name: name,
reqToken: reqToken,
silent: silentAuth,
tenant: tenant,
}
}
// GetToken ensures that only one goroutine authenticates at a time
func (s *syncer) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
var at azcore.AccessToken
var err error
if len(opts.Scopes) == 0 {
return at, errors.New(s.name + ".GetToken() requires at least one scope")
}
// we don't resolve the tenant for managed identities because they can acquire tokens only from their home tenants
if s.name != credNameManagedIdentity {
tenant, err := s.resolveTenant(opts.TenantID)
if err != nil {
return at, err
}
opts.TenantID = tenant
}
auth := false
s.cond.L.Lock()
defer s.cond.L.Unlock()
for {
at, err = s.silent(ctx, opts)
if err == nil {
// got a token
break
}
if !s.authing {
// this goroutine will request a token
s.authing, auth = true, true
break
}
// another goroutine is acquiring a token; wait for it to finish, then try silent auth again
s.cond.Wait()
}
if auth {
s.authing = false
at, err = s.reqToken(ctx, opts)
s.cond.Broadcast()
}
if err != nil {
// Return credentialUnavailableError directly because that type affects the behavior of credential chains.
// Otherwise, return AuthenticationFailedError.
var unavailableErr *credentialUnavailableError
if !errors.As(err, &unavailableErr) {
res := getResponseFromError(err)
err = newAuthenticationFailedError(s.name, err.Error(), res, err)
}
} else if log.Should(EventAuthentication) {
scope := strings.Join(opts.Scopes, ", ")
msg := fmt.Sprintf(`%s.GetToken() acquired a token for scope "%s"\n`, s.name, scope)
log.Write(EventAuthentication, msg)
}
return at, err
}
// resolveTenant returns the correct tenant for a token request given the credential's
// configuration, or an error when the specified tenant isn't allowed by that configuration
func (s *syncer) resolveTenant(requested string) (string, error) {
if requested == "" || requested == s.tenant {
return s.tenant, nil
}
if s.tenant == "adfs" {
return "", errors.New("ADFS doesn't support tenants")
}
if !validTenantID(requested) {
return "", errors.New(tenantIDValidationErr)
}
for _, t := range s.addlTenants {
if t == "*" || t == requested {
return requested, nil
}
}
return "", fmt.Errorf(`%s isn't configured to acquire tokens for tenant %q. To enable acquiring tokens for this tenant add it to the AdditionallyAllowedTenants on the credential options, or add "*" to allow acquiring tokens for any tenant`, s.name, requested)
}
// resolveAdditionalTenants returns a copy of tenants, simplified when tenants contains a wildcard
func resolveAdditionalTenants(tenants []string) []string {
if len(tenants) == 0 {
return nil
}
for _, t := range tenants {
// a wildcard makes all other values redundant
if t == "*" {
return []string{"*"}
}
}
cp := make([]string, len(tenants))
copy(cp, tenants)
return cp
}

View File

@@ -8,7 +8,6 @@ package azidentity
import (
"context"
"errors"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
@@ -20,6 +19,16 @@ const credNameUserPassword = "UsernamePasswordCredential"
// UsernamePasswordCredentialOptions contains optional parameters for UsernamePasswordCredential.
type UsernamePasswordCredentialOptions 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 should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
}
// UsernamePasswordCredential authenticates a user with a password. Microsoft doesn't recommend this kind of authentication,
@@ -27,10 +36,10 @@ type UsernamePasswordCredentialOptions struct {
// with any form of multi-factor authentication, and the application must already have user or admin consent.
// This credential can only authenticate work and school accounts; it can't authenticate Microsoft accounts.
type UsernamePasswordCredential struct {
client publicClient
username string
password string
account public.Account
account public.Account
client publicClient
password, username string
s *syncer
}
// NewUsernamePasswordCredential creates a UsernamePasswordCredential. clientID is the ID of the application the user
@@ -39,29 +48,33 @@ func NewUsernamePasswordCredential(tenantID string, clientID string, username st
if options == nil {
options = &UsernamePasswordCredentialOptions{}
}
c, err := getPublicClient(clientID, tenantID, &options.ClientOptions)
c, err := getPublicClient(clientID, tenantID, &options.ClientOptions, public.WithInstanceDiscovery(!options.DisableInstanceDiscovery))
if err != nil {
return nil, err
}
return &UsernamePasswordCredential{username: username, password: password, client: c}, nil
upc := UsernamePasswordCredential{client: c, password: password, username: username}
upc.s = newSyncer(credNameUserPassword, tenantID, options.AdditionallyAllowedTenants, upc.requestToken, upc.silentAuth)
return &upc, nil
}
// GetToken requests an access token from Azure Active Directory. This method is called automatically by Azure SDK clients.
func (c *UsernamePasswordCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
if len(opts.Scopes) == 0 {
return azcore.AccessToken{}, errors.New(credNameUserPassword + ": GetToken() requires at least one scope")
}
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes, public.WithSilentAccount(c.account))
return c.s.GetToken(ctx, opts)
}
func (c *UsernamePasswordCredential) requestToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenByUsernamePassword(ctx, opts.Scopes, c.username, c.password, public.WithTenantID(opts.TenantID))
if err == nil {
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
c.account = ar.Account
}
ar, err = c.client.AcquireTokenByUsernamePassword(ctx, opts.Scopes, c.username, c.password)
if err != nil {
return azcore.AccessToken{}, newAuthenticationFailedErrorFromMSALError(credNameUserPassword, err)
}
c.account = ar.Account
logGetTokenSuccess(c, opts)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}
func (c *UsernamePasswordCredential) silentAuth(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
ar, err := c.client.AcquireTokenSilent(ctx, opts.Scopes,
public.WithSilentAccount(c.account),
public.WithTenantID(opts.TenantID),
)
return azcore.AccessToken{Token: ar.AccessToken, ExpiresOn: ar.ExpiresOn.UTC()}, err
}

View File

@@ -11,5 +11,5 @@ const (
component = "azidentity"
// Version is the semantic version (see http://semver.org) of this module.
version = "v1.2.1"
version = "v1.3.0"
)

View File

@@ -0,0 +1,126 @@
//go:build go1.18
// +build go1.18
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package azidentity
import (
"context"
"errors"
"os"
"sync"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
)
const credNameWorkloadIdentity = "WorkloadIdentityCredential"
// WorkloadIdentityCredential supports Azure workload identity on Kubernetes.
// See [Azure Kubernetes Service documentation] for more information.
//
// [Azure Kubernetes Service documentation]: https://learn.microsoft.com/azure/aks/workload-identity-overview
type WorkloadIdentityCredential struct {
assertion, file string
cred *ClientAssertionCredential
expires time.Time
mtx *sync.RWMutex
}
// 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
// ClientID of the service principal. Defaults to the value of the environment variable AZURE_CLIENT_ID.
ClientID string
// DisableInstanceDiscovery should be set true only by applications authenticating in disconnected clouds, or
// private clouds such as Azure Stack. It determines whether the credential requests Azure AD instance metadata
// from https://login.microsoft.com before authenticating. Setting this to true will skip this request, making
// the application responsible for ensuring the configured authority is valid and trustworthy.
DisableInstanceDiscovery bool
// TenantID of the service principal. Defaults to the value of the environment variable AZURE_TENANT_ID.
TenantID string
// TokenFilePath is the path a file containing the workload identity token. Defaults to the value of the
// environment variable AZURE_FEDERATED_TOKEN_FILE.
TokenFilePath string
}
// NewWorkloadIdentityCredential constructs a WorkloadIdentityCredential. Service principal configuration is read
// from environment variables as set by the Azure workload identity webhook. Set options to override those values.
func NewWorkloadIdentityCredential(options *WorkloadIdentityCredentialOptions) (*WorkloadIdentityCredential, error) {
if options == nil {
options = &WorkloadIdentityCredentialOptions{}
}
ok := false
clientID := options.ClientID
if clientID == "" {
if clientID, ok = os.LookupEnv(azureClientID); !ok {
return nil, errors.New("no client ID specified. Check pod configuration or set ClientID in the options")
}
}
file := options.TokenFilePath
if file == "" {
if file, ok = os.LookupEnv(azureFederatedTokenFile); !ok {
return nil, errors.New("no token file specified. Check pod configuration or set TokenFilePath in the options")
}
}
tenantID := options.TenantID
if tenantID == "" {
if tenantID, ok = os.LookupEnv(azureTenantID); !ok {
return nil, errors.New("no tenant ID specified. Check pod configuration or set TenantID in the options")
}
}
w := WorkloadIdentityCredential{file: file, mtx: &sync.RWMutex{}}
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
}
// we want "WorkloadIdentityCredential" in log messages, not "ClientAssertionCredential"
cred.s.name = credNameWorkloadIdentity
w.cred = cred
return &w, nil
}
// GetToken requests an access token from Azure Active Directory. Azure SDK clients call this method automatically.
func (w *WorkloadIdentityCredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
return w.cred.GetToken(ctx, opts)
}
// getAssertion returns the specified file's content, which is expected to be a Kubernetes service account token.
// Kubernetes is responsible for updating the file as service account tokens expire.
func (w *WorkloadIdentityCredential) getAssertion(context.Context) (string, error) {
w.mtx.RLock()
if w.expires.Before(time.Now()) {
// ensure only one goroutine at a time updates the assertion
w.mtx.RUnlock()
w.mtx.Lock()
defer w.mtx.Unlock()
// double check because another goroutine may have acquired the write lock first and done the update
if now := time.Now(); w.expires.Before(now) {
content, err := os.ReadFile(w.file)
if err != nil {
return "", err
}
w.assertion = string(content)
// Kubernetes rotates service account tokens when they reach 80% of their total TTL. The shortest TTL
// is 1 hour. That implies the token we just read is valid for at least 12 minutes (20% of 1 hour),
// but we add some margin for safety.
w.expires = now.Add(10 * time.Minute)
}
} else {
defer w.mtx.RUnlock()
}
return w.assertion, nil
}