github.com/marksheahan/packer@v0.10.2-0.20160613200515-1acb2d6645a0/builder/azure/common/devicelogin.go (about)

     1  package common
     2  
     3  import (
     4  	"fmt"
     5  	"net/http"
     6  	"os"
     7  	"path/filepath"
     8  	"regexp"
     9  
    10  	"github.com/Azure/azure-sdk-for-go/arm/resources/subscriptions"
    11  	"github.com/Azure/go-autorest/autorest"
    12  	"github.com/Azure/go-autorest/autorest/azure"
    13  	"github.com/Azure/go-autorest/autorest/to"
    14  	"github.com/mitchellh/go-homedir"
    15  	"github.com/mitchellh/packer/version"
    16  )
    17  
    18  var (
    19  	// AD app id for packer-azure driver.
    20  	clientIDs = map[string]string{
    21  		azure.PublicCloud.Name: "04cc58ec-51ab-4833-ac0d-ce3a7912414b",
    22  	}
    23  
    24  	userAgent = fmt.Sprintf("packer/%s", version.FormattedVersion())
    25  )
    26  
    27  // NOTE(ahmetalpbalkan): Azure Active Directory implements OAuth 2.0 Device Flow
    28  // described here: https://tools.ietf.org/html/draft-denniss-oauth-device-flow-00
    29  // Although it has some gotchas, most of the authentication logic is in Azure SDK
    30  // for Go helper packages.
    31  //
    32  // Device auth prints a message to the screen telling the user to click on URL
    33  // and approve the app on the browser, meanwhile the client polls the auth API
    34  // for a token. Once we have token, we save it locally to a file with proper
    35  // permissions and when the token expires (in Azure case typically 1 hour) SDK
    36  // will automatically refresh the specified token and will call the refresh
    37  // callback function we implement here. This way we will always be storing a
    38  // token with a refresh_token saved on the machine.
    39  
    40  // Authenticate fetches a token from the local file cache or initiates a consent
    41  // flow and waits for token to be obtained.
    42  func Authenticate(env azure.Environment, subscriptionID string, say func(string)) (*azure.ServicePrincipalToken, error) {
    43  	clientID, ok := clientIDs[env.Name]
    44  	if !ok {
    45  		return nil, fmt.Errorf("packer-azure application not set up for Azure environment %q", env.Name)
    46  	}
    47  
    48  	// First we locate the tenant ID of the subscription as we store tokens per
    49  	// tenant (which could have multiple subscriptions)
    50  	say(fmt.Sprintf("Looking up AAD Tenant ID: subscriptionID=%s.", subscriptionID))
    51  	tenantID, err := findTenantID(env, subscriptionID)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	say(fmt.Sprintf("Found AAD Tenant ID: tenantID=%s", tenantID))
    56  
    57  	oauthCfg, err := env.OAuthConfigForTenant(tenantID)
    58  	if err != nil {
    59  		return nil, fmt.Errorf("Failed to obtain oauth config for azure environment: %v", err)
    60  	}
    61  
    62  	// for AzurePublicCloud (https://management.core.windows.net/), this old
    63  	// Service Management scope covers both ASM and ARM.
    64  	apiScope := env.ServiceManagementEndpoint
    65  
    66  	tokenPath := tokenCachePath(tenantID)
    67  	saveToken := mkTokenCallback(tokenPath)
    68  	saveTokenCallback := func(t azure.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, apiScope, 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  
    81  		// NOTE(ahmetalpbalkan): The token file we found may contain an
    82  		// expired access_token. In that case, the first call to Azure SDK will
    83  		// attempt to refresh the token using refresh_token, which might have
    84  		// expired[1], in that case we will get an error and we shall remove the
    85  		// token file and initiate token flow again so that the user would not
    86  		// need removing the token cache file manually.
    87  		//
    88  		// [1]: expiration date of refresh_token is not returned in AAD /token
    89  		//      response, we just know it is 14 days. Therefore user’s token
    90  		//      will go stale every 14 days and we will delete the token file,
    91  		//      re-initiate the device flow.
    92  		say("Validating the token.")
    93  		if err := validateToken(env, spt); err != nil {
    94  			say(fmt.Sprintf("Error: %v", err))
    95  			say("Stored Azure credentials expired. Please reauthenticate.")
    96  			say(fmt.Sprintf("Deleting %s", tokenPath))
    97  			if err := os.RemoveAll(tokenPath); err != nil {
    98  				return nil, fmt.Errorf("Error deleting stale token file: %v", err)
    99  			}
   100  		} else {
   101  			say("Token works.")
   102  			return spt, nil
   103  		}
   104  	}
   105  
   106  	// Start an OAuth 2.0 device flow
   107  	say(fmt.Sprintf("Initiating device flow: %s", tokenPath))
   108  	spt, err = tokenFromDeviceFlow(say, *oauthCfg, tokenPath, clientID, apiScope)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	say("Obtained service principal token.")
   113  	if err := saveToken(spt.Token); err != nil {
   114  		say("Error occurred saving token to cache file.")
   115  		return nil, err
   116  	}
   117  	return spt, nil
   118  }
   119  
   120  // tokenFromFile returns a token from the specified file if it is found, otherwise
   121  // returns nil. Any error retrieving or creating the token is returned as an error.
   122  func tokenFromFile(say func(string), oauthCfg azure.OAuthConfig, tokenPath, clientID, resource string,
   123  	callback azure.TokenRefreshCallback) (*azure.ServicePrincipalToken, error) {
   124  	say(fmt.Sprintf("Loading auth token from file: %s", tokenPath))
   125  	if _, err := os.Stat(tokenPath); err != nil {
   126  		if os.IsNotExist(err) { // file not found
   127  			return nil, nil
   128  		}
   129  		return nil, err
   130  	}
   131  
   132  	token, err := azure.LoadToken(tokenPath)
   133  	if err != nil {
   134  		return nil, fmt.Errorf("Failed to load token from file: %v", err)
   135  	}
   136  
   137  	spt, err := azure.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token, callback)
   138  	if err != nil {
   139  		return nil, fmt.Errorf("Error constructing service principal token: %v", err)
   140  	}
   141  	return spt, nil
   142  }
   143  
   144  // tokenFromDeviceFlow prints a message to the screen for user to take action to
   145  // consent application on a browser and in the meanwhile the authentication
   146  // endpoint is polled until user gives consent, denies or the flow times out.
   147  // Returned token must be saved.
   148  func tokenFromDeviceFlow(say func(string), oauthCfg azure.OAuthConfig, tokenPath, clientID, resource string) (*azure.ServicePrincipalToken, error) {
   149  	cl := autorest.NewClientWithUserAgent(userAgent)
   150  	deviceCode, err := azure.InitiateDeviceAuth(&cl, oauthCfg, clientID, resource)
   151  	if err != nil {
   152  		return nil, fmt.Errorf("Failed to start device auth: %v", err)
   153  	}
   154  
   155  	// Example message: “To sign in, open https://aka.ms/devicelogin and enter
   156  	// the code 0000000 to authenticate.”
   157  	say(fmt.Sprintf("Microsoft Azure: %s", to.String(deviceCode.Message)))
   158  
   159  	token, err := azure.WaitForUserCompletion(&cl, deviceCode)
   160  	if err != nil {
   161  		return nil, fmt.Errorf("Failed to complete device auth: %v", err)
   162  	}
   163  
   164  	spt, err := azure.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token)
   165  	if err != nil {
   166  		return nil, fmt.Errorf("Error constructing service principal token: %v", err)
   167  	}
   168  	return spt, nil
   169  }
   170  
   171  // tokenCachePath returns the full path the OAuth 2.0 token should be saved at
   172  // for given tenant ID.
   173  func tokenCachePath(tenantID string) string {
   174  	dir, err := homedir.Dir()
   175  	if err != nil {
   176  		dir, _ = filepath.Abs(os.Args[0])
   177  	}
   178  
   179  	return filepath.Join(dir, ".azure", "packer", fmt.Sprintf("oauth-%s.json", tenantID))
   180  }
   181  
   182  // mkTokenCallback returns a callback function that can be used to save the
   183  // token initially or register to the Azure SDK to be called when the token is
   184  // refreshed.
   185  func mkTokenCallback(path string) azure.TokenRefreshCallback {
   186  	return func(t azure.Token) error {
   187  		if err := azure.SaveToken(path, 0600, t); err != nil {
   188  			return err
   189  		}
   190  		return nil
   191  	}
   192  }
   193  
   194  // validateToken makes a call to Azure SDK with given token, essentially making
   195  // sure if the access_token valid, if not it uses SDK’s functionality to
   196  // automatically refresh the token using refresh_token (which might have
   197  // expired). This check is essentially to make sure refresh_token is good.
   198  func validateToken(env azure.Environment, token *azure.ServicePrincipalToken) error {
   199  	c := subscriptionsClient(env.ResourceManagerEndpoint)
   200  	c.Authorizer = token
   201  	_, err := c.List()
   202  	if err != nil {
   203  		return fmt.Errorf("Token validity check failed: %v", err)
   204  	}
   205  	return nil
   206  }
   207  
   208  // findTenantID figures out the AAD tenant ID of the subscription by making an
   209  // unauthenticated request to the Get Subscription Details endpoint and parses
   210  // the value from WWW-Authenticate header.
   211  func findTenantID(env azure.Environment, subscriptionID string) (string, error) {
   212  	const hdrKey = "WWW-Authenticate"
   213  	c := subscriptionsClient(env.ResourceManagerEndpoint)
   214  
   215  	// we expect this request to fail (err != nil), but we are only interested
   216  	// in headers, so surface the error if the Response is not present (i.e.
   217  	// network error etc)
   218  	subs, err := c.Get(subscriptionID)
   219  	if subs.Response.Response == nil {
   220  		return "", fmt.Errorf("Request failed: %v", err)
   221  	}
   222  
   223  	// Expecting 401 StatusUnauthorized here, just read the header
   224  	if subs.StatusCode != http.StatusUnauthorized {
   225  		return "", fmt.Errorf("Unexpected response from Get Subscription: %v", err)
   226  	}
   227  	hdr := subs.Header.Get(hdrKey)
   228  	if hdr == "" {
   229  		return "", fmt.Errorf("Header %v not found in Get Subscription response", hdrKey)
   230  	}
   231  
   232  	// Example value for hdr:
   233  	//   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."
   234  	r := regexp.MustCompile(`authorization_uri=".*/([0-9a-f\-]+)"`)
   235  	m := r.FindStringSubmatch(hdr)
   236  	if m == nil {
   237  		return "", fmt.Errorf("Could not find the tenant ID in header: %s %q", hdrKey, hdr)
   238  	}
   239  	return m[1], nil
   240  }
   241  
   242  func subscriptionsClient(baseURI string) subscriptions.Client {
   243  	c := subscriptions.NewClientWithBaseURI(baseURI, "") // used only for unauthenticated requests for generic subs IDs
   244  	c.Client.UserAgent += userAgent
   245  	return c
   246  }