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 }