//go:build go1.18 // +build go1.18 // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. package azidentity import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "strings" "sync" "time" "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" ) const credNameAzureCLI = "AzureCLICredential" // AzureCLICredentialOptions contains optional parameters for AzureCLICredential. type AzureCLICredentialOptions struct { // AdditionallyAllowedTenants specifies tenants to which the credential may authenticate, in addition to // TenantID. When TenantID is empty, this option has no effect and the credential will authenticate to // any requested tenant. Add the wildcard value "*" to allow the credential to authenticate to any tenant. AdditionallyAllowedTenants []string // Subscription is the name or ID of a subscription. Set this to acquire tokens for an account other // than the Azure CLI's current account. Subscription 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 // inDefaultChain is true when the credential is part of DefaultAzureCredential inDefaultChain bool // exec is used by tests to fake invoking az exec executor } // AzureCLICredential authenticates as the identity logged in to the Azure CLI. type AzureCLICredential struct { mu *sync.Mutex opts AzureCLICredentialOptions } // NewAzureCLICredential constructs an AzureCLICredential. Pass nil to accept default options. func NewAzureCLICredential(options *AzureCLICredentialOptions) (*AzureCLICredential, error) { cp := AzureCLICredentialOptions{} if options != nil { cp = *options } for _, r := range cp.Subscription { if !(alphanumeric(r) || r == '-' || r == '_' || r == ' ' || r == '.') { return nil, fmt.Errorf( "%s: Subscription %q contains invalid characters. If this is the name of a subscription, use its ID instead", credNameAzureCLI, cp.Subscription, ) } } if cp.TenantID != "" && !validTenantID(cp.TenantID) { return nil, errInvalidTenantID } if cp.exec == nil { cp.exec = shellExec } cp.AdditionallyAllowedTenants = resolveAdditionalTenants(cp.AdditionallyAllowedTenants) return &AzureCLICredential{mu: &sync.Mutex{}, opts: cp}, nil } // GetToken requests a token from the Azure CLI. This credential doesn't cache tokens, so every call invokes the CLI. // This method is called automatically by Azure SDK clients. func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) { at := azcore.AccessToken{} if len(opts.Scopes) != 1 { return at, errors.New(credNameAzureCLI + ": GetToken() requires exactly one scope") } if !validScope(opts.Scopes[0]) { return at, fmt.Errorf("%s.GetToken(): invalid scope %q", credNameAzureCLI, opts.Scopes[0]) } tenant, err := resolveTenant(c.opts.TenantID, opts.TenantID, credNameAzureCLI, c.opts.AdditionallyAllowedTenants) if err != nil { return at, err } // pass the CLI a Microsoft Entra ID v1 resource because we don't know which CLI version is installed and older ones don't support v2 scopes resource := strings.TrimSuffix(opts.Scopes[0], defaultSuffix) command := "az account get-access-token -o json --resource " + resource tenantArg := "" if tenant != "" { tenantArg = " --tenant " + tenant command += tenantArg } if c.opts.Subscription != "" { // subscription needs quotes because it may contain spaces command += ` --subscription "` + c.opts.Subscription + `"` } if opts.Claims != "" { encoded := base64.StdEncoding.EncodeToString([]byte(opts.Claims)) return at, fmt.Errorf( "%s.GetToken(): Azure CLI requires multifactor authentication or additional claims. Run this command then retry the operation: az login%s --claims-challenge %s", credNameAzureCLI, tenantArg, encoded, ) } c.mu.Lock() defer c.mu.Unlock() b, err := c.opts.exec(ctx, credNameAzureCLI, command) if err == nil { at, err = c.createAccessToken(b) } if err != nil { err = unavailableIfInDAC(err, c.opts.inDefaultChain) return at, err } msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", credNameAzureCLI, strings.Join(opts.Scopes, ", ")) log.Write(EventAuthentication, msg) return at, nil } func (c *AzureCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, error) { t := struct { AccessToken string `json:"accessToken"` Expires_On int64 `json:"expires_on"` ExpiresOn string `json:"expiresOn"` }{} err := json.Unmarshal(tk, &t) if err != nil { return azcore.AccessToken{}, err } exp := time.Unix(t.Expires_On, 0) if t.Expires_On == 0 { exp, err = time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local) if err != nil { return azcore.AccessToken{}, fmt.Errorf("%s: error parsing token expiration time %q: %v", credNameAzureCLI, t.ExpiresOn, err) } } converted := azcore.AccessToken{ Token: t.AccessToken, ExpiresOn: exp.UTC(), } return converted, nil } var _ azcore.TokenCredential = (*AzureCLICredential)(nil)