/
devicelogin.go
203 lines (177 loc) · 7.29 KB
/
devicelogin.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
package common
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-06-01/subscriptions"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
"github.com/Azure/go-autorest/autorest/azure"
"github.com/Azure/go-autorest/autorest/to"
"github.com/hashicorp/packer/helper/useragent"
"github.com/mitchellh/go-homedir"
)
var (
// AD app id for packer-azure driver.
clientIDs = map[string]string{
azure.PublicCloud.Name: "04cc58ec-51ab-4833-ac0d-ce3a7912414b",
azure.USGovernmentCloud.Name: "a1479822-da77-46a7-abd0-6edacc8a8fac",
}
)
// NOTE(ahmetalpbalkan): Azure Active Directory implements OAuth 2.0 Device Flow
// described here: https://tools.ietf.org/html/draft-denniss-oauth-device-flow-00
// Although it has some gotchas, most of the authentication logic is in Azure SDK
// for Go helper packages.
//
// 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.
// Authenticate fetches a token from the local file cache or initiates a consent
// flow and waits for token to be obtained.
func Authenticate(env azure.Environment, tenantID string, say func(string), scope string) (*adal.ServicePrincipalToken, error) {
clientID, ok := clientIDs[env.Name]
var resourceid string
if !ok {
return nil, fmt.Errorf("packer-azure application not set up for Azure environment %q", env.Name)
}
oauthCfg, err := adal.NewOAuthConfig(env.ActiveDirectoryEndpoint, tenantID)
if err != nil {
return nil, fmt.Errorf("Failed to obtain oauth config for azure environment: %v", err)
}
// for AzurePublicCloud (https://management.core.windows.net/), this old
// Service Management scope covers both ASM and ARM.
if strings.Contains(scope, "vault") {
resourceid = "vault"
} else {
resourceid = "mgmt"
}
tokenPath := tokenCachePath(tenantID + resourceid)
saveToken := mkTokenCallback(tokenPath)
saveTokenCallback := func(t adal.Token) error {
say("Azure token expired. Saving the refreshed token...")
return saveToken(t)
}
// Lookup the token cache file for an existing token.
spt, err := tokenFromFile(say, *oauthCfg, tokenPath, clientID, scope, saveTokenCallback)
if err != nil {
return nil, err
}
if spt != nil {
say(fmt.Sprintf("Auth token found in file: %s", tokenPath))
return spt, nil
}
// Start an OAuth 2.0 device flow
say(fmt.Sprintf("Initiating device flow: %s", tokenPath))
spt, err = tokenFromDeviceFlow(say, *oauthCfg, clientID, scope)
if err != nil {
return nil, err
}
say("Obtained service principal token.")
if err := saveToken(spt.Token()); err != nil {
say("Error occurred saving token to cache file.")
return nil, 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(say func(string), oauthCfg adal.OAuthConfig, tokenPath, clientID, resource string,
callback adal.TokenRefreshCallback) (*adal.ServicePrincipalToken, error) {
say(fmt.Sprintf("Loading auth token from file: %s", tokenPath))
if _, err := os.Stat(tokenPath); err != nil {
if os.IsNotExist(err) { // file not found
return nil, nil
}
return nil, err
}
token, err := adal.LoadToken(tokenPath)
if err != nil {
return nil, fmt.Errorf("Failed to load token from file: %v", err)
}
spt, err := adal.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token, callback)
if err != nil {
return nil, fmt.Errorf("Error constructing service principal token: %v", err)
}
return spt, nil
}
// tokenFromDeviceFlow 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 tokenFromDeviceFlow(say func(string), oauthCfg adal.OAuthConfig, clientID, resource string) (*adal.ServicePrincipalToken, error) {
cl := autorest.NewClientWithUserAgent(useragent.String())
deviceCode, err := adal.InitiateDeviceAuth(&cl, oauthCfg, clientID, resource)
if err != nil {
return nil, fmt.Errorf("Failed to start device auth: %v", err)
}
// Example message: “To sign in, open https://aka.ms/devicelogin and enter
// the code 0000000 to authenticate.”
say(fmt.Sprintf("Microsoft Azure: %s", to.String(deviceCode.Message)))
token, err := adal.WaitForUserCompletion(&cl, deviceCode)
if err != nil {
return nil, fmt.Errorf("Failed to complete device auth: %v", err)
}
spt, err := adal.NewServicePrincipalTokenFromManualToken(oauthCfg, clientID, resource, *token)
if err != nil {
return nil, fmt.Errorf("Error constructing service principal token: %v", err)
}
return spt, nil
}
// tokenCachePath returns the full path the OAuth 2.0 token should be saved at
// for given tenant ID.
func tokenCachePath(tenantID string) string {
dir, err := homedir.Dir()
if err != nil {
dir, _ = filepath.Abs(os.Args[0])
}
return filepath.Join(dir, ".azure", "packer", fmt.Sprintf("oauth-%s.json", tenantID))
}
// 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) adal.TokenRefreshCallback {
return func(t adal.Token) error {
if err := adal.SaveToken(path, 0600, t); err != nil {
return err
}
return nil
}
}
// FindTenantID figures out the AAD tenant ID of the subscription by making an
// unauthenticated request to the Get Subscription Details endpoint and parses
// the value from WWW-Authenticate header.
func FindTenantID(env azure.Environment, subscriptionID string) (string, error) {
const hdrKey = "WWW-Authenticate"
c := subscriptions.NewClientWithBaseURI(env.ResourceManagerEndpoint)
// we expect this request to fail (err != nil), but we are only interested
// in headers, so surface the error if the Response is not present (i.e.
// network error etc)
subs, err := c.Get(context.TODO(), subscriptionID)
if subs.Response.Response == nil {
return "", fmt.Errorf("Request failed: %v", err)
}
// Expecting 401 StatusUnauthorized here, just read the header
if subs.StatusCode != http.StatusUnauthorized {
return "", fmt.Errorf("Unexpected response from Get Subscription: %v", err)
}
hdr := subs.Header.Get(hdrKey)
if hdr == "" {
return "", fmt.Errorf("Header %v not found in Get Subscription response", hdrKey)
}
// Example value for hdr:
// 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."
r := regexp.MustCompile(`authorization_uri=".*/([0-9a-f\-]+)"`)
m := r.FindStringSubmatch(hdr)
if m == nil {
return "", fmt.Errorf("Could not find the tenant ID in header: %s %q", hdrKey, hdr)
}
return m[1], nil
}