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 }