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