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