github.com/mitchellh/packer@v1.3.2/builder/azure/common/devicelogin.go (about)

     1  package common
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  	"path/filepath"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-06-01/subscriptions"
    13  	"github.com/Azure/go-autorest/autorest"
    14  	"github.com/Azure/go-autorest/autorest/adal"
    15  	"github.com/Azure/go-autorest/autorest/azure"
    16  	"github.com/Azure/go-autorest/autorest/to"
    17  	"github.com/hashicorp/packer/helper/useragent"
    18  	"github.com/mitchellh/go-homedir"
    19  )
    20  
    21  var (
    22  	// AD app id for packer-azure driver.
    23  	clientIDs = map[string]string{
    24  		azure.PublicCloud.Name:       "04cc58ec-51ab-4833-ac0d-ce3a7912414b",
    25  		azure.USGovernmentCloud.Name: "a1479822-da77-46a7-abd0-6edacc8a8fac",
    26  	}
    27  )
    28  
    29  // NOTE(ahmetalpbalkan): Azure Active Directory implements OAuth 2.0 Device Flow
    30  // described here: https://tools.ietf.org/html/draft-denniss-oauth-device-flow-00
    31  // Although it has some gotchas, most of the authentication logic is in Azure SDK
    32  // for Go helper packages.
    33  //
    34  // Device auth prints a message to the screen telling the user to click on URL
    35  // and approve the app on the browser, meanwhile the client polls the auth API
    36  // for a token. Once we have token, we save it locally to a file with proper
    37  // permissions and when the token expires (in Azure case typically 1 hour) SDK
    38  // will automatically refresh the specified token and will call the refresh
    39  // callback function we implement here. This way we will always be storing a
    40  // token with a refresh_token saved on the machine.
    41  
    42  // Authenticate fetches a token from the local file cache or initiates a consent
    43  // flow and waits for token to be obtained.
    44  func Authenticate(env azure.Environment, tenantID string, say func(string), scope string) (*adal.ServicePrincipalToken, error) {
    45  	clientID, ok := clientIDs[env.Name]
    46  	var resourceid string
    47  
    48  	if !ok {
    49  		return nil, fmt.Errorf("packer-azure application not set up for Azure environment %q", env.Name)
    50  	}
    51  
    52  	oauthCfg, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, tenantID)
    53  	if err != nil {
    54  		return nil, fmt.Errorf("Failed to obtain oauth config for azure environment: %v", err)
    55  	}
    56  
    57  	// for AzurePublicCloud (https://management.core.windows.net/), this old
    58  	// Service Management scope covers both ASM and ARM.
    59  
    60  	if strings.Contains(scope, "vault") {
    61  		resourceid = "vault"
    62  	} else {
    63  		resourceid = "mgmt"
    64  	}
    65  
    66  	tokenPath := tokenCachePath(tenantID + resourceid)
    67  	saveToken := mkTokenCallback(tokenPath)
    68  	saveTokenCallback := func(t adal.Token) error {
    69  		say("Azure token expired. Saving the refreshed token...")
    70  		return saveToken(t)
    71  	}
    72  
    73  	// Lookup the token cache file for an existing token.
    74  	spt, err := tokenFromFile(say, *oauthCfg, tokenPath, clientID, scope, saveTokenCallback)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	if spt != nil {
    79  		say(fmt.Sprintf("Auth token found in file: %s", tokenPath))
    80  		return spt, nil
    81  	}
    82  
    83  	// Start an OAuth 2.0 device flow
    84  	say(fmt.Sprintf("Initiating device flow: %s", tokenPath))
    85  	spt, err = tokenFromDeviceFlow(say, *oauthCfg, clientID, scope)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	say("Obtained service principal token.")
    90  	if err := saveToken(spt.Token()); err != nil {
    91  		say("Error occurred saving token to cache file.")
    92  		return nil, err
    93  	}
    94  	return spt, nil
    95  }
    96  
    97  // tokenFromFile returns a token from the specified file if it is found, otherwise
    98  // returns nil. Any error retrieving or creating the token is returned as an error.
    99  func tokenFromFile(say func(string), oauthCfg adal.OAuthConfig, tokenPath, clientID, resource string,
   100  	callback adal.TokenRefreshCallback) (*adal.ServicePrincipalToken, error) {
   101  	say(fmt.Sprintf("Loading auth token from file: %s", tokenPath))
   102  	if _, err := os.Stat(tokenPath); err != nil {
   103  		if os.IsNotExist(err) { // file not found
   104  			return nil, nil
   105  		}
   106  		return nil, err
   107  	}
   108  
   109  	token, err := adal.LoadToken(tokenPath)
   110  	if err != nil {
   111  		return nil, fmt.Errorf("Failed to load token from file: %v", err)
   112  	}
   113  
   114  	spt, err := adal.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token, callback)
   115  	if err != nil {
   116  		return nil, fmt.Errorf("Error constructing service principal token: %v", err)
   117  	}
   118  	return spt, nil
   119  }
   120  
   121  // tokenFromDeviceFlow prints a message to the screen for user to take action to
   122  // consent application on a browser and in the meanwhile the authentication
   123  // endpoint is polled until user gives consent, denies or the flow times out.
   124  // Returned token must be saved.
   125  func tokenFromDeviceFlow(say func(string), oauthCfg adal.OAuthConfig, clientID, resource string) (*adal.ServicePrincipalToken, error) {
   126  	cl := autorest.NewClientWithUserAgent(useragent.String())
   127  	deviceCode, err := adal.InitiateDeviceAuth(&cl, oauthCfg, clientID, resource)
   128  	if err != nil {
   129  		return nil, fmt.Errorf("Failed to start device auth: %v", err)
   130  	}
   131  
   132  	// Example message: “To sign in, open https://aka.ms/devicelogin and enter
   133  	// the code 0000000 to authenticate.”
   134  	say(fmt.Sprintf("Microsoft Azure: %s", to.String(deviceCode.Message)))
   135  
   136  	token, err := adal.WaitForUserCompletion(&cl, deviceCode)
   137  	if err != nil {
   138  		return nil, fmt.Errorf("Failed to complete device auth: %v", err)
   139  	}
   140  
   141  	spt, err := adal.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token)
   142  	if err != nil {
   143  		return nil, fmt.Errorf("Error constructing service principal token: %v", err)
   144  	}
   145  	return spt, nil
   146  }
   147  
   148  // tokenCachePath returns the full path the OAuth 2.0 token should be saved at
   149  // for given tenant ID.
   150  func tokenCachePath(tenantID string) string {
   151  	dir, err := homedir.Dir()
   152  	if err != nil {
   153  		dir, _ = filepath.Abs(os.Args[0])
   154  	}
   155  
   156  	return filepath.Join(dir, ".azure", "packer", fmt.Sprintf("oauth-%s.json", tenantID))
   157  }
   158  
   159  // mkTokenCallback returns a callback function that can be used to save the
   160  // token initially or register to the Azure SDK to be called when the token is
   161  // refreshed.
   162  func mkTokenCallback(path string) adal.TokenRefreshCallback {
   163  	return func(t adal.Token) error {
   164  		if err := adal.SaveToken(path, 0600, t); err != nil {
   165  			return err
   166  		}
   167  		return nil
   168  	}
   169  }
   170  
   171  // FindTenantID figures out the AAD tenant ID of the subscription by making an
   172  // unauthenticated request to the Get Subscription Details endpoint and parses
   173  // the value from WWW-Authenticate header.
   174  func FindTenantID(env azure.Environment, subscriptionID string) (string, error) {
   175  	const hdrKey = "WWW-Authenticate"
   176  	c := subscriptions.NewClientWithBaseURI(env.ResourceManagerEndpoint)
   177  
   178  	// we expect this request to fail (err != nil), but we are only interested
   179  	// in headers, so surface the error if the Response is not present (i.e.
   180  	// network error etc)
   181  	subs, err := c.Get(context.TODO(), subscriptionID)
   182  	if subs.Response.Response == nil {
   183  		return "", fmt.Errorf("Request failed: %v", err)
   184  	}
   185  
   186  	// Expecting 401 StatusUnauthorized here, just read the header
   187  	if subs.StatusCode != http.StatusUnauthorized {
   188  		return "", fmt.Errorf("Unexpected response from Get Subscription: %v", err)
   189  	}
   190  	hdr := subs.Header.Get(hdrKey)
   191  	if hdr == "" {
   192  		return "", fmt.Errorf("Header %v not found in Get Subscription response", hdrKey)
   193  	}
   194  
   195  	// Example value for hdr:
   196  	//   Bearer authorization_uri="https://login.windows.net/996fe9d1-6171-40aa-945b-4c64b63bf655", error="invalid_token", error_description="The authentication failed because of missing 'Authorization' header."
   197  	r := regexp.MustCompile(`authorization_uri=".*/([0-9a-f\-]+)"`)
   198  	m := r.FindStringSubmatch(hdr)
   199  	if m == nil {
   200  		return "", fmt.Errorf("Could not find the tenant ID in header: %s %q", hdrKey, hdr)
   201  	}
   202  	return m[1], nil
   203  }