errors.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  	"bytes"
  11  	"encoding/json"
  12  	"errors"
  13  	"fmt"
  14  	"net/http"
  15  
  16  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
  17  	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
  18  	"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
  19  	msal "github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
  20  )
  21  
  22  // getResponseFromError retrieves the response carried by
  23  // an AuthenticationFailedError or MSAL CallErr, if any
  24  func getResponseFromError(err error) *http.Response {
  25  	var a *AuthenticationFailedError
  26  	var c msal.CallErr
  27  	var res *http.Response
  28  	if errors.As(err, &c) {
  29  		res = c.Resp
  30  	} else if errors.As(err, &a) {
  31  		res = a.RawResponse
  32  	}
  33  	return res
  34  }
  35  
  36  // AuthenticationFailedError indicates an authentication request has failed.
  37  type AuthenticationFailedError struct {
  38  	// RawResponse is the HTTP response motivating the error, if available.
  39  	RawResponse *http.Response
  40  
  41  	credType, message string
  42  	omitResponse      bool
  43  }
  44  
  45  func newAuthenticationFailedError(credType, message string, resp *http.Response) error {
  46  	return &AuthenticationFailedError{credType: credType, message: message, RawResponse: resp}
  47  }
  48  
  49  // newAuthenticationFailedErrorFromMSAL creates an AuthenticationFailedError from an MSAL error.
  50  // If the error is an MSAL CallErr, the new error includes an HTTP response and not the MSAL error
  51  // message, because that message is redundant given the response. If the original error isn't a
  52  // CallErr, the returned error incorporates its message.
  53  func newAuthenticationFailedErrorFromMSAL(credType string, err error) error {
  54  	msg := ""
  55  	res := getResponseFromError(err)
  56  	if res == nil {
  57  		msg = err.Error()
  58  	}
  59  	return newAuthenticationFailedError(credType, msg, res)
  60  }
  61  
  62  // Error implements the error interface. Note that the message contents are not contractual and can change over time.
  63  func (e *AuthenticationFailedError) Error() string {
  64  	if e.RawResponse == nil || e.omitResponse {
  65  		return e.credType + ": " + e.message
  66  	}
  67  	msg := &bytes.Buffer{}
  68  	fmt.Fprintf(msg, "%s authentication failed. %s\n", e.credType, e.message)
  69  	if e.RawResponse.Request != nil {
  70  		fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.Path)
  71  	} else {
  72  		// this happens when the response is created from a custom HTTP transporter,
  73  		// which doesn't guarantee to bind the original request to the response
  74  		fmt.Fprintln(msg, "Request information not available")
  75  	}
  76  	fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
  77  	fmt.Fprintf(msg, "RESPONSE %d: %s\n", e.RawResponse.StatusCode, e.RawResponse.Status)
  78  	fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
  79  	body, err := runtime.Payload(e.RawResponse)
  80  	switch {
  81  	case err != nil:
  82  		fmt.Fprintf(msg, "Error reading response body: %v", err)
  83  	case len(body) > 0:
  84  		if err := json.Indent(msg, body, "", "  "); err != nil {
  85  			// failed to pretty-print so just dump it verbatim
  86  			fmt.Fprint(msg, string(body))
  87  		}
  88  	default:
  89  		fmt.Fprint(msg, "Response contained no body")
  90  	}
  91  	fmt.Fprintln(msg, "\n--------------------------------------------------------------------------------")
  92  	var anchor string
  93  	switch e.credType {
  94  	case credNameAzureCLI:
  95  		anchor = "azure-cli"
  96  	case credNameAzureDeveloperCLI:
  97  		anchor = "azd"
  98  	case credNameAzurePipelines:
  99  		anchor = "apc"
 100  	case credNameCert:
 101  		anchor = "client-cert"
 102  	case credNameAzurePowerShell:
 103  		anchor = "azure-pwsh"
 104  	case credNameSecret:
 105  		anchor = "client-secret"
 106  	case credNameManagedIdentity:
 107  		anchor = "managed-id"
 108  	case credNameWorkloadIdentity:
 109  		anchor = "workload"
 110  	}
 111  	if anchor != "" {
 112  		fmt.Fprintf(msg, "To troubleshoot, visit https://aka.ms/azsdk/go/identity/troubleshoot#%s", anchor)
 113  	}
 114  	return msg.String()
 115  }
 116  
 117  // NonRetriable indicates the request which provoked this error shouldn't be retried.
 118  func (*AuthenticationFailedError) NonRetriable() {
 119  	// marker method
 120  }
 121  
 122  var _ errorinfo.NonRetriable = (*AuthenticationFailedError)(nil)
 123  
 124  // AuthenticationRequiredError indicates a credential's Authenticate method must be called to acquire a token
 125  // because the credential requires user interaction and is configured not to request it automatically.
 126  type AuthenticationRequiredError struct {
 127  	credentialUnavailableError
 128  
 129  	// TokenRequestOptions for the required token. Pass this to the credential's Authenticate method.
 130  	TokenRequestOptions policy.TokenRequestOptions
 131  }
 132  
 133  func newAuthenticationRequiredError(credType string, tro policy.TokenRequestOptions) error {
 134  	return &AuthenticationRequiredError{
 135  		credentialUnavailableError: credentialUnavailableError{
 136  			credType + " can't acquire a token without user interaction. Call Authenticate to authenticate a user interactively",
 137  		},
 138  		TokenRequestOptions: tro,
 139  	}
 140  }
 141  
 142  var (
 143  	_ credentialUnavailable  = (*AuthenticationRequiredError)(nil)
 144  	_ errorinfo.NonRetriable = (*AuthenticationRequiredError)(nil)
 145  )
 146  
 147  type credentialUnavailable interface {
 148  	error
 149  	credentialUnavailable()
 150  }
 151  
 152  type credentialUnavailableError struct {
 153  	message string
 154  }
 155  
 156  // newCredentialUnavailableError is an internal helper that ensures consistent error message formatting
 157  func newCredentialUnavailableError(credType, message string) error {
 158  	msg := fmt.Sprintf("%s: %s", credType, message)
 159  	return &credentialUnavailableError{msg}
 160  }
 161  
 162  // NewCredentialUnavailableError constructs an error indicating a credential can't attempt authentication
 163  // because it lacks required data or state. When [ChainedTokenCredential] receives this error it will try
 164  // its next credential, if any.
 165  func NewCredentialUnavailableError(message string) error {
 166  	return &credentialUnavailableError{message}
 167  }
 168  
 169  // Error implements the error interface. Note that the message contents are not contractual and can change over time.
 170  func (e *credentialUnavailableError) Error() string {
 171  	return e.message
 172  }
 173  
 174  // NonRetriable is a marker method indicating this error should not be retried. It has no implementation.
 175  func (*credentialUnavailableError) NonRetriable() {}
 176  
 177  func (*credentialUnavailableError) credentialUnavailable() {}
 178  
 179  var (
 180  	_ credentialUnavailable  = (*credentialUnavailableError)(nil)
 181  	_ errorinfo.NonRetriable = (*credentialUnavailableError)(nil)
 182  )
 183