azure_cli_credential.go raw

   1  //go:build go1.18
   2  // +build go1.18
   3  
   4  // Copyright (c) Microsoft Corporation. All rights reserved.
   5  // Licensed under the MIT License.
   6  
   7  package azidentity
   8  
   9  import (
  10  	"context"
  11  	"encoding/base64"
  12  	"encoding/json"
  13  	"errors"
  14  	"fmt"
  15  	"strings"
  16  	"sync"
  17  	"time"
  18  
  19  	"github.com/Azure/azure-sdk-for-go/sdk/azcore"
  20  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
  21  	"github.com/Azure/azure-sdk-for-go/sdk/internal/log"
  22  )
  23  
  24  const credNameAzureCLI = "AzureCLICredential"
  25  
  26  // AzureCLICredentialOptions contains optional parameters for AzureCLICredential.
  27  type AzureCLICredentialOptions struct {
  28  	// AdditionallyAllowedTenants specifies tenants to which the credential may authenticate, in addition to
  29  	// TenantID. When TenantID is empty, this option has no effect and the credential will authenticate to
  30  	// any requested tenant. Add the wildcard value "*" to allow the credential to authenticate to any tenant.
  31  	AdditionallyAllowedTenants []string
  32  
  33  	// Subscription is the name or ID of a subscription. Set this to acquire tokens for an account other
  34  	// than the Azure CLI's current account.
  35  	Subscription string
  36  
  37  	// TenantID identifies the tenant the credential should authenticate in.
  38  	// Defaults to the CLI's default tenant, which is typically the home tenant of the logged in user.
  39  	TenantID string
  40  
  41  	// inDefaultChain is true when the credential is part of DefaultAzureCredential
  42  	inDefaultChain bool
  43  	// exec is used by tests to fake invoking az
  44  	exec executor
  45  }
  46  
  47  // AzureCLICredential authenticates as the identity logged in to the Azure CLI.
  48  type AzureCLICredential struct {
  49  	mu   *sync.Mutex
  50  	opts AzureCLICredentialOptions
  51  }
  52  
  53  // NewAzureCLICredential constructs an AzureCLICredential. Pass nil to accept default options.
  54  func NewAzureCLICredential(options *AzureCLICredentialOptions) (*AzureCLICredential, error) {
  55  	cp := AzureCLICredentialOptions{}
  56  	if options != nil {
  57  		cp = *options
  58  	}
  59  	for _, r := range cp.Subscription {
  60  		if !(alphanumeric(r) || r == '-' || r == '_' || r == ' ' || r == '.') {
  61  			return nil, fmt.Errorf(
  62  				"%s: Subscription %q contains invalid characters. If this is the name of a subscription, use its ID instead",
  63  				credNameAzureCLI,
  64  				cp.Subscription,
  65  			)
  66  		}
  67  	}
  68  	if cp.TenantID != "" && !validTenantID(cp.TenantID) {
  69  		return nil, errInvalidTenantID
  70  	}
  71  	if cp.exec == nil {
  72  		cp.exec = shellExec
  73  	}
  74  	cp.AdditionallyAllowedTenants = resolveAdditionalTenants(cp.AdditionallyAllowedTenants)
  75  	return &AzureCLICredential{mu: &sync.Mutex{}, opts: cp}, nil
  76  }
  77  
  78  // GetToken requests a token from the Azure CLI. This credential doesn't cache tokens, so every call invokes the CLI.
  79  // This method is called automatically by Azure SDK clients.
  80  func (c *AzureCLICredential) GetToken(ctx context.Context, opts policy.TokenRequestOptions) (azcore.AccessToken, error) {
  81  	at := azcore.AccessToken{}
  82  	if len(opts.Scopes) != 1 {
  83  		return at, errors.New(credNameAzureCLI + ": GetToken() requires exactly one scope")
  84  	}
  85  	if !validScope(opts.Scopes[0]) {
  86  		return at, fmt.Errorf("%s.GetToken(): invalid scope %q", credNameAzureCLI, opts.Scopes[0])
  87  	}
  88  	tenant, err := resolveTenant(c.opts.TenantID, opts.TenantID, credNameAzureCLI, c.opts.AdditionallyAllowedTenants)
  89  	if err != nil {
  90  		return at, err
  91  	}
  92  	// 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
  93  	resource := strings.TrimSuffix(opts.Scopes[0], defaultSuffix)
  94  	command := "az account get-access-token -o json --resource " + resource
  95  	tenantArg := ""
  96  	if tenant != "" {
  97  		tenantArg = " --tenant " + tenant
  98  		command += tenantArg
  99  	}
 100  	if c.opts.Subscription != "" {
 101  		// subscription needs quotes because it may contain spaces
 102  		command += ` --subscription "` + c.opts.Subscription + `"`
 103  	}
 104  	if opts.Claims != "" {
 105  		encoded := base64.StdEncoding.EncodeToString([]byte(opts.Claims))
 106  		return at, fmt.Errorf(
 107  			"%s.GetToken(): Azure CLI requires multifactor authentication or additional claims. Run this command then retry the operation: az login%s --claims-challenge %s",
 108  			credNameAzureCLI,
 109  			tenantArg,
 110  			encoded,
 111  		)
 112  	}
 113  
 114  	c.mu.Lock()
 115  	defer c.mu.Unlock()
 116  
 117  	b, err := c.opts.exec(ctx, credNameAzureCLI, command)
 118  	if err == nil {
 119  		at, err = c.createAccessToken(b)
 120  	}
 121  	if err != nil {
 122  		err = unavailableIfInDAC(err, c.opts.inDefaultChain)
 123  		return at, err
 124  	}
 125  	msg := fmt.Sprintf("%s.GetToken() acquired a token for scope %q", credNameAzureCLI, strings.Join(opts.Scopes, ", "))
 126  	log.Write(EventAuthentication, msg)
 127  	return at, nil
 128  }
 129  
 130  func (c *AzureCLICredential) createAccessToken(tk []byte) (azcore.AccessToken, error) {
 131  	t := struct {
 132  		AccessToken string `json:"accessToken"`
 133  		Expires_On  int64  `json:"expires_on"`
 134  		ExpiresOn   string `json:"expiresOn"`
 135  	}{}
 136  	err := json.Unmarshal(tk, &t)
 137  	if err != nil {
 138  		return azcore.AccessToken{}, err
 139  	}
 140  
 141  	exp := time.Unix(t.Expires_On, 0)
 142  	if t.Expires_On == 0 {
 143  		exp, err = time.ParseInLocation("2006-01-02 15:04:05.999999", t.ExpiresOn, time.Local)
 144  		if err != nil {
 145  			return azcore.AccessToken{}, fmt.Errorf("%s: error parsing token expiration time %q: %v", credNameAzureCLI, t.ExpiresOn, err)
 146  		}
 147  	}
 148  
 149  	converted := azcore.AccessToken{
 150  		Token:     t.AccessToken,
 151  		ExpiresOn: exp.UTC(),
 152  	}
 153  	return converted, nil
 154  }
 155  
 156  var _ azcore.TokenCredential = (*AzureCLICredential)(nil)
 157