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