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