1package azureutil 2 3import ( 4 "fmt" 5 "os" 6 "path/filepath" 7 8 "github.com/docker/machine/libmachine/log" 9 "github.com/docker/machine/libmachine/mcnutils" 10 11 "github.com/Azure/go-autorest/autorest/azure" 12 "github.com/Azure/go-autorest/autorest/to" 13 "github.com/docker/machine/drivers/azure/logutil" 14) 15 16// Azure driver allows two authentication methods: 17// 18// 1. OAuth Device Flow 19// 20// Azure Active Directory implements OAuth 2.0 Device Flow described here: 21// https://tools.ietf.org/html/draft-denniss-oauth-device-flow-00. It is simple 22// for users to authenticate through a browser and requires re-authenticating 23// every 2 weeks. 24// 25// Device auth prints a message to the screen telling the user to click on URL 26// and approve the app on the browser, meanwhile the client polls the auth API 27// for a token. Once we have token, we save it locally to a file with proper 28// permissions and when the token expires (in Azure case typically 1 hour) SDK 29// will automatically refresh the specified token and will call the refresh 30// callback function we implement here. This way we will always be storing a 31// token with a refresh_token saved on the machine. 32// 33// 2. Azure Service Principal Account 34// 35// This is designed for headless authentication to Azure APIs but requires more 36// steps from user to create a Service Principal Account and provide its 37// credentials to the machine driver. 38 39var ( 40 // AD app id for docker-machine driver in various Azure realms 41 appIDs = map[string]string{ 42 azure.PublicCloud.Name: "637ddaba-219b-43b8-bf19-8cea500cf273", 43 azure.ChinaCloud.Name: "bb5eed6f-120b-4365-8fd9-ab1a3fba5698", 44 azure.GermanCloud.Name: "aabac5f7-dd47-47ef-824c-e0d57598cada", 45 } 46) 47 48// AuthenticateDeviceFlow fetches a token from the local file cache or initiates a consent 49// flow and waits for token to be obtained. Obtained token is stored in a file cache for 50// future use and refreshing. 51func AuthenticateDeviceFlow(env azure.Environment, subscriptionID string) (*azure.ServicePrincipalToken, error) { 52 // First we locate the tenant ID of the subscription as we store tokens per 53 // tenant (which could have multiple subscriptions) 54 tenantID, err := loadOrFindTenantID(env, subscriptionID) 55 if err != nil { 56 return nil, err 57 } 58 oauthCfg, err := env.OAuthConfigForTenant(tenantID) 59 if err != nil { 60 return nil, fmt.Errorf("Failed to obtain oauth config for azure environment: %v", err) 61 } 62 63 tokenPath := tokenCachePath(tenantID) 64 saveToken := mkTokenCallback(tokenPath) 65 saveTokenCallback := func(t azure.Token) error { 66 log.Debug("Azure token expired. Saving the refreshed token...") 67 return saveToken(t) 68 } 69 f := logutil.Fields{"path": tokenPath} 70 71 appID, ok := appIDs[env.Name] 72 if !ok { 73 return nil, fmt.Errorf("docker-machine application not set up for Azure environment %q", env.Name) 74 } 75 scope := getScope(env) 76 77 // Lookup the token cache file for an existing token. 78 spt, err := tokenFromFile(*oauthCfg, tokenPath, appID, scope, saveTokenCallback) 79 if err != nil { 80 return nil, err 81 } 82 if spt != nil { 83 log.Debug("Auth token found in file.", f) 84 85 // NOTE(ahmetalpbalkan): The token file we found might be containing an 86 // expired access_token. In that case, the first call to Azure SDK will 87 // attempt to refresh the token using refresh_token –which might have 88 // expired[1], in that case we will get an error and we shall remove the 89 // token file and initiate token flow again so that the user would not 90 // need removing the token cache file manually. 91 // 92 // [1]: for device flow auth, the expiration date of refresh_token is 93 // not returned in AAD /token response, we just know it is 14 94 // days. Therefore user’s token will go stale every 14 days and we 95 // will delete the token file, re-initiate the device flow. Service 96 // Principal Account tokens are not subject to this limitation. 97 log.Debug("Validating the token.") 98 if err := validateToken(env, spt); err != nil { 99 log.Debug(fmt.Sprintf("Error: %v", err)) 100 log.Debug(fmt.Sprintf("Deleting %s", tokenPath)) 101 if err := os.RemoveAll(tokenPath); err != nil { 102 return nil, fmt.Errorf("Error deleting stale token file: %v", err) 103 } 104 } else { 105 log.Debug("Token works.") 106 return spt, nil 107 } 108 } 109 110 log.Debug("Obtaining a token.", f) 111 spt, err = deviceFlowAuth(*oauthCfg, appID, scope) 112 if err != nil { 113 return nil, err 114 } 115 log.Debug("Obtained a token.") 116 if err := saveToken(spt.Token); err != nil { 117 log.Error("Error occurred saving token to cache file.") 118 return nil, err 119 } 120 return spt, nil 121} 122 123// AuthenticateServicePrincipal uses given service principal credentials to return a 124// service principal token. Generated token is not stored in a cache file or refreshed. 125func AuthenticateServicePrincipal(env azure.Environment, subscriptionID, spID, spPassword string) (*azure.ServicePrincipalToken, error) { 126 tenantID, err := loadOrFindTenantID(env, subscriptionID) 127 if err != nil { 128 return nil, err 129 } 130 oauthCfg, err := env.OAuthConfigForTenant(tenantID) 131 if err != nil { 132 return nil, fmt.Errorf("Failed to obtain oauth config for azure environment: %v", err) 133 } 134 135 spt, err := azure.NewServicePrincipalToken(*oauthCfg, spID, spPassword, getScope(env)) 136 if err != nil { 137 return nil, fmt.Errorf("Failed to create service principal token: %+v", err) 138 } 139 return spt, nil 140} 141 142// tokenFromFile returns a token from the specified file if it is found, otherwise 143// returns nil. Any error retrieving or creating the token is returned as an error. 144func tokenFromFile(oauthCfg azure.OAuthConfig, tokenPath, clientID, resource string, 145 callback azure.TokenRefreshCallback) (*azure.ServicePrincipalToken, error) { 146 log.Debug("Loading auth token from file", logutil.Fields{"path": tokenPath}) 147 if _, err := os.Stat(tokenPath); err != nil { 148 if os.IsNotExist(err) { // file not found 149 return nil, nil 150 } 151 return nil, err 152 } 153 154 token, err := azure.LoadToken(tokenPath) 155 if err != nil { 156 return nil, fmt.Errorf("Failed to load token from file: %v", err) 157 } 158 159 spt, err := azure.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token, callback) 160 if err != nil { 161 return nil, fmt.Errorf("Error constructing service principal token: %v", err) 162 } 163 return spt, nil 164} 165 166// deviceFlowAuth prints a message to the screen for user to take action to 167// consent application on a browser and in the meanwhile the authentication 168// endpoint is polled until user gives consent, denies or the flow times out. 169// Returned token must be saved. 170func deviceFlowAuth(oauthCfg azure.OAuthConfig, clientID, resource string) (*azure.ServicePrincipalToken, error) { 171 cl := oauthClient() 172 deviceCode, err := azure.InitiateDeviceAuth(&cl, oauthCfg, clientID, resource) 173 if err != nil { 174 return nil, fmt.Errorf("Failed to start device auth: %v", err) 175 } 176 log.Debug("Retrieved device code.", logutil.Fields{ 177 "expires_in": to.Int64(deviceCode.ExpiresIn), 178 "interval": to.Int64(deviceCode.Interval), 179 }) 180 181 // Example message: “To sign in, open https://aka.ms/devicelogin and enter 182 // the code 0000000 to authenticate.” 183 log.Infof("Microsoft Azure: %s", to.String(deviceCode.Message)) 184 185 token, err := azure.WaitForUserCompletion(&cl, deviceCode) 186 if err != nil { 187 return nil, fmt.Errorf("Failed to complete device auth: %v", err) 188 } 189 190 spt, err := azure.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token) 191 if err != nil { 192 return nil, fmt.Errorf("Error constructing service principal token: %v", err) 193 } 194 return spt, nil 195} 196 197// azureCredsPath returns the directory the azure credentials are stored in. 198func azureCredsPath() string { 199 return filepath.Join(mcnutils.GetHomeDir(), ".docker", "machine", "credentials", "azure") 200} 201 202// tokenCachePath returns the full path the OAuth 2.0 token should be saved at 203// for given tenant ID. 204func tokenCachePath(tenantID string) string { 205 return filepath.Join(azureCredsPath(), fmt.Sprintf("%s.json", tenantID)) 206} 207 208// tenantIDPath returns the full path the tenant ID for the given subscription 209// should be saved at.f 210func tenantIDPath(subscriptionID string) string { 211 return filepath.Join(azureCredsPath(), fmt.Sprintf("%s.tenantid", subscriptionID)) 212} 213 214// mkTokenCallback returns a callback function that can be used to save the 215// token initially or register to the Azure SDK to be called when the token is 216// refreshed. 217func mkTokenCallback(path string) azure.TokenRefreshCallback { 218 return func(t azure.Token) error { 219 if err := azure.SaveToken(path, 0600, t); err != nil { 220 return err 221 } 222 log.Debug("Saved token to file.") 223 return nil 224 } 225} 226 227// validateToken makes a call to Azure SDK with given token, essentially making 228// sure if the access_token valid, if not it uses SDK’s functionality to 229// automatically refresh the token using refresh_token (which might have 230// expired). This check is essentially to make sure refresh_token is good. 231func validateToken(env azure.Environment, token *azure.ServicePrincipalToken) error { 232 c := subscriptionsClient(env.ResourceManagerEndpoint) 233 c.Authorizer = token 234 _, err := c.List() 235 if err != nil { 236 return fmt.Errorf("Token validity check failed: %v", err) 237 } 238 return nil 239} 240 241// getScope returns the API scope for authentication tokens. 242func getScope(env azure.Environment) string { 243 // for AzurePublicCloud (https://management.core.windows.net/), this old 244 // Service Management scope covers both ASM and ARM. 245 return env.ServiceManagementEndpoint 246} 247