@@ -17,18 +17,26 @@ limitations under the License.
17
17
package azure
18
18
19
19
import (
20
+ "bytes"
21
+ "fmt"
22
+ "io"
23
+ "net/http"
24
+ "strings"
20
25
"testing"
21
26
22
- "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy "
27
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
23
28
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
24
29
"github.com/stretchr/testify/require"
25
30
)
26
31
27
32
func TestNewCredential (t * testing.T ) {
28
- options := policy.ClientOptions {}
33
+ creds := map [string ]string {}
34
+ options := azcore.ClientOptions {Transport : & mockSTS {tenant : "" ,
35
+ tokenRequestCallback : func (r * http.Request ) {
36
+
37
+ }}}
29
38
30
39
// no credentials
31
- creds := map [string ]string {}
32
40
_ , err := NewCredential (creds , options )
33
41
require .NotNil (t , err )
34
42
@@ -94,3 +102,165 @@ func Test_newConfigCredential(t *testing.T) {
94
102
_ , ok = credential .(* azidentity.UsernamePasswordCredential )
95
103
require .True (t , ok )
96
104
}
105
+
106
+ const (
107
+ mockClientInfo = "eyJ1aWQiOiJjNzNjNmYyOC1hZTVmLTQxM2QtYTlhMi1lMTFlNWFmNjY4ZjgiLCJ1dGlkIjoiZTBiZDIzMjEtMDdmYS00Y2YwLTg3YjgtMDBhYTJhNzQ3MzI5In0"
108
+ mockIDT = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Imwzc1EtNTBjQ0g0eEJWWkxIVEd3blNSNzY4MCJ9.eyJhdWQiOiIwNGIwNzc5NS04ZGRiLTQ2MWEtYmJlZS0wMmY5ZTFiZjdiNDYiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vYzU0ZmFjODgtM2RkMy00NjFmLWE3YzQtOGEzNjhlMDM0MGIzL3YyLjAiLCJpYXQiOjE2MzcxOTEyMTIsIm5iZiI6MTYzNzE5MTIxMiwiZXhwIjoxNjM3MTk1MTEyLCJhaW8iOiJBVVFBdS84VEFBQUFQMExOZGNRUXQxNmJoSkFreXlBdjFoUGJuQVhtT0o3RXJDVHV4N0hNTjhHd2VMb2FYMWR1cDJhQ2Y0a0p5bDFzNmovSzF5R05DZmVIQlBXM21QUWlDdz09IiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZTBiZDIzMjEtMDdmYS00Y2YwLTg3YjgtMDBhYTJhNzQ3MzI5LyIsIm5hbWUiOiJJZGVudGl0eSBUZXN0IFVzZXIiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJpZGVudGl0eXRlc3R1c2VyQGF6dXJlc2Rrb3V0bG9vay5vbm1pY3Jvc29mdC5jb20iLCJyaCI6IjAuQVMwQWlLeFB4ZE05SDBhbnhJbzJqZ05BczVWM3NBVGJqUnBHdS00Qy1lR19lMFl0QUxFLiIsInN1YiI6ImMxYTBsY2xtbWxCYW9wc0MwVmlaLVpPMjFCT2dSUXE3SG9HRUtOOXloZnMiLCJ0aWQiOiJjNTRmYWM4OC0zZGQzLTQ2MWYtYTdjNC04YTM2OGUwMzQwYjMiLCJ1dGkiOiI5TXFOSWI5WjdrQy1QVHRtai11X0FBIiwidmVyIjoiMi4wIn0.hh5Exz9MBjTXrTuTZnz7vceiuQjcC_oRSTeBIC9tYgSO2c2sqQRpZi91qBZFQD9okayLPPKcwqXgEJD9p0-c4nUR5UQN7YSeDLmYtZUYMG79EsA7IMiQaiy94AyIe2E-oBDcLwFycGwh1iIOwwOwjbanmu2Dx3HfQx831lH9uVjagf0Aow0wTkTVCsedGSZvG-cRUceFLj-kFN-feFH3NuScuOfLR2Magf541pJda7X7oStwL_RNUFqjJFTdsiFV4e-VHK5qo--3oPU06z0rS9bosj0pFSATIVHrrS4gY7jiSvgMbG837CDBQkz5b08GUN5GlLN9jlygl1plBmbgww"
109
+
110
+ fakeClientID = "fake-client-id"
111
+ fakeResourceID = "/fake/resource/ID"
112
+ fakeTenantID = "fake-tenant"
113
+ fakeUsername = "fake@user"
114
+ fakeAdfsAuthority = "fake.adfs.local"
115
+ fakeAdfsScope = "fake.adfs.local/fake-scope/.default"
116
+
117
+ tokenExpiresIn = 3600
118
+ tokenValue = "new_token"
119
+ )
120
+
121
+ var (
122
+ accessTokenRespSuccess = []byte (fmt .Sprintf (`{"access_token": "%s", "expires_in": %d}` , tokenValue , tokenExpiresIn ))
123
+ )
124
+
125
+ func getInstanceDiscoveryResponse (tenant string ) []byte {
126
+ return []byte (strings .ReplaceAll (`{
127
+ "tenant_discovery_endpoint": "https://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration",
128
+ "api-version": "1.1",
129
+ "metadata": [
130
+ {
131
+ "preferred_network": "login.microsoftonline.com",
132
+ "preferred_cache": "login.windows.net",
133
+ "aliases": [
134
+ "login.microsoftonline.com",
135
+ "login.windows.net",
136
+ "login.microsoft.com",
137
+ "sts.windows.net"
138
+ ]
139
+ }
140
+ ]
141
+ }` , "{tenant}" , tenant ))
142
+ }
143
+
144
+ func getTenantDiscoveryResponse (tenant string ) []byte {
145
+ return []byte (strings .ReplaceAll (`{
146
+ "token_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token",
147
+ "token_endpoint_auth_methods_supported": [
148
+ "client_secret_post",
149
+ "private_key_jwt",
150
+ "client_secret_basic"
151
+ ],
152
+ "jwks_uri": "https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys",
153
+ "response_modes_supported": [
154
+ "query",
155
+ "fragment",
156
+ "form_post"
157
+ ],
158
+ "subject_types_supported": [
159
+ "pairwise"
160
+ ],
161
+ "id_token_signing_alg_values_supported": [
162
+ "RS256"
163
+ ],
164
+ "response_types_supported": [
165
+ "code",
166
+ "id_token",
167
+ "code id_token",
168
+ "id_token token"
169
+ ],
170
+ "scopes_supported": [
171
+ "openid",
172
+ "profile",
173
+ "email",
174
+ "offline_access"
175
+ ],
176
+ "issuer": "https://login.microsoftonline.com/{tenant}/v2.0",
177
+ "request_uri_parameter_supported": false,
178
+ "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo",
179
+ "authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize",
180
+ "device_authorization_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/devicecode",
181
+ "http_logout_supported": true,
182
+ "frontchannel_logout_supported": true,
183
+ "end_session_endpoint": "https://login.microsoftonline.com/{tenant}/oauth2/v2.0/logout",
184
+ "claims_supported": [
185
+ "sub",
186
+ "iss",
187
+ "cloud_instance_name",
188
+ "cloud_instance_host_name",
189
+ "cloud_graph_host_name",
190
+ "msgraph_host",
191
+ "aud",
192
+ "exp",
193
+ "iat",
194
+ "auth_time",
195
+ "acr",
196
+ "nonce",
197
+ "preferred_username",
198
+ "name",
199
+ "tid",
200
+ "ver",
201
+ "at_hash",
202
+ "c_hash",
203
+ "email"
204
+ ],
205
+ "kerberos_endpoint": "https://login.microsoftonline.com/{tenant}/kerberos",
206
+ "tenant_region_scope": "NA",
207
+ "cloud_instance_name": "microsoftonline.com",
208
+ "cloud_graph_host_name": "graph.windows.net",
209
+ "msgraph_host": "graph.microsoft.com",
210
+ "rbac_url": "https://pas.windows.net"
211
+ }` , "{tenant}" , tenant ))
212
+ }
213
+
214
+ // mockSTS returns mock Azure AD responses so tests don't have to account for
215
+ // MSAL metadata requests. All responses are success responses. Mock access
216
+ // tokens expire in 1 hour and have the value of the "tokenValue" constant.
217
+ type mockSTS struct {
218
+ // tenant to include in metadata responses. This value must match a test's
219
+ // expected tenant because metadata tells MSAL where to send token requests.
220
+ // Defaults to the "fakeTenantID" constant.
221
+ tenant string
222
+ // tokenRequestCallback is called for every token request
223
+ tokenRequestCallback func (* http.Request )
224
+ }
225
+
226
+ func (m * mockSTS ) Do (req * http.Request ) (* http.Response , error ) {
227
+ res := http.Response {StatusCode : http .StatusOK }
228
+ tenant := m .tenant
229
+ if tenant == "" {
230
+ tenant = fakeTenantID
231
+ }
232
+ switch s := strings .Split (req .URL .Path , "/" ); s [len (s )- 1 ] {
233
+ case "instance" :
234
+ res .Body = io .NopCloser (bytes .NewReader (getInstanceDiscoveryResponse (tenant )))
235
+ case "openid-configuration" :
236
+ res .Body = io .NopCloser (bytes .NewReader (getTenantDiscoveryResponse (tenant )))
237
+ case "devicecode" :
238
+ res .Body = io .NopCloser (strings .NewReader (`{"device_code":"...","expires_in":600,"interval":60}` ))
239
+ case "token" :
240
+ if m .tokenRequestCallback != nil {
241
+ m .tokenRequestCallback (req )
242
+ }
243
+ if err := req .ParseForm (); err != nil {
244
+ return nil , fmt .Errorf ("mockSTS failed to parse a request body: %w" , err )
245
+ }
246
+ if grant := req .FormValue ("grant_type" ); grant == "device_code" || grant == "password" {
247
+ // include account info because we're authenticating a user
248
+ res .Body = io .NopCloser (bytes .NewReader (
249
+ []byte (fmt .Sprintf (`{"access_token":"at","expires_in": 3600,"refresh_token":"rt","client_info":%q,"id_token":%q}` , mockClientInfo , mockIDT )),
250
+ ))
251
+ } else {
252
+ res .Body = io .NopCloser (bytes .NewReader (accessTokenRespSuccess ))
253
+ }
254
+ default :
255
+ // User realm metadata request paths look like "/common/UserRealm/user@domain".
256
+ // Matching on the UserRealm segment avoids having to know the UPN.
257
+ if s [len (s )- 2 ] == "UserRealm" {
258
+ res .Body = io .NopCloser (
259
+ strings .NewReader (`{"account_type":"Managed","cloud_audience_urn":"urn","cloud_instance_name":"...","domain_name":"..."}` ),
260
+ )
261
+ } else {
262
+ return nil , fmt .Errorf ("mockSTS received an unexpected request for %s" , req .URL .String ())
263
+ }
264
+ }
265
+ return & res , nil
266
+ }
0 commit comments