Skip to content

Commit 637a056

Browse files
committed
oauth: add unit tests for OAuth2Client
1 parent fca0adc commit 637a056

File tree

7 files changed

+871
-16
lines changed

7 files changed

+871
-16
lines changed

src/shared/Microsoft.AzureRepos.Tests/AzureDevOpsApiTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -329,13 +329,13 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcRetu
329329
var identSvcError = CreateIdentityServiceErrorResponse(serverErrorMessage);
330330

331331
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
332-
httpHandler.Setup(HttpMethod.Get, locSvcRequestUri, x =>
332+
httpHandler.Setup(HttpMethod.Get, locSvcRequestUri, x =>
333333
{
334334
AssertAcceptJson(x);
335335
AssertBearerToken(x, accessToken);
336336
return locSvcResponse;
337337
});
338-
httpHandler.Setup(HttpMethod.Post, identSvcRequestUri, _ => identSvcError);
338+
httpHandler.Setup(HttpMethod.Post, identSvcRequestUri, identSvcError);
339339

340340
context.HttpClientFactory.MessageHandler = httpHandler;
341341
var api = new AzureDevOpsRestApi(context);
Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,319 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Net.Http;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using Microsoft.Git.CredentialManager.Authentication.OAuth;
8+
using Microsoft.Git.CredentialManager.Tests.Objects;
9+
using Xunit;
10+
11+
namespace Microsoft.Git.CredentialManager.Tests.Authentication
12+
{
13+
public class OAuth2ClientTests
14+
{
15+
private const string TestClientId = "9ffe7f11c8";
16+
private const string TestClientSecret = "62adac63a4614d93833470942a38454f";
17+
private static readonly Uri TestRedirectUri = new Uri("http://localhost/oauth-callback");
18+
19+
[Fact]
20+
public async Task OAuth2Client_GetAuthorizationCodeAsync()
21+
{
22+
const string expectedAuthCode = "68c39cbd8d";
23+
24+
var baseUri = new Uri("https://example.com");
25+
OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
26+
27+
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
28+
29+
string[] expectedScopes = {"read", "write", "delete"};
30+
31+
OAuth2Application app = CreateTestApplication();
32+
33+
var server = new TestOAuth2Server(endpoints);
34+
server.RegisterApplication(app);
35+
server.Bind(httpHandler);
36+
server.TokenGenerator.AuthCodes.Add(expectedAuthCode);
37+
38+
IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);
39+
40+
OAuth2Client client = CreateClient(httpHandler, endpoints);
41+
42+
string actualAuthCode = await client.GetAuthorizationCodeAsync(expectedScopes, browser, CancellationToken.None);
43+
44+
Assert.Equal(expectedAuthCode, actualAuthCode);
45+
}
46+
47+
[Fact]
48+
public async Task OAuth2Client_GetDeviceCodeAsync()
49+
{
50+
const string expectedUserCode = "254583";
51+
const string expectedDeviceCode = "6d1e34151aff4f41b9f186e177a0b15d";
52+
53+
var baseUri = new Uri("https://example.com");
54+
OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
55+
56+
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
57+
58+
string[] expectedScopes = {"read", "write", "delete"};
59+
60+
OAuth2Application app = CreateTestApplication();
61+
62+
var server = new TestOAuth2Server(endpoints);
63+
server.RegisterApplication(app);
64+
server.Bind(httpHandler);
65+
server.TokenGenerator.UserCodes.Add(expectedUserCode);
66+
server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode);
67+
68+
OAuth2Client client = CreateClient(httpHandler, endpoints);
69+
70+
OAuth2DeviceCodeResult result = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None);
71+
72+
Assert.Equal(expectedUserCode, result.UserCode);
73+
Assert.Equal(expectedDeviceCode, result.DeviceCode);
74+
}
75+
76+
[Fact]
77+
public async Task OAuth2Client_GetTokenByAuthorizationCodeAsync()
78+
{
79+
const string authCode = "a63ef59691";
80+
const string expectedAccessToken = "LET_ME_IN";
81+
const string expectedRefreshToken = "REFRESH_ME";
82+
83+
var baseUri = new Uri("https://example.com");
84+
OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
85+
86+
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
87+
88+
string[] expectedScopes = {"read", "write", "delete"};
89+
90+
OAuth2Application app = CreateTestApplication();
91+
app.AuthCodes[authCode] = expectedScopes;
92+
93+
var server = new TestOAuth2Server(endpoints);
94+
server.RegisterApplication(app);
95+
server.Bind(httpHandler);
96+
server.TokenGenerator.AccessTokens.Add(expectedAccessToken);
97+
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);
98+
99+
OAuth2Client client = CreateClient(httpHandler, endpoints);
100+
101+
OAuth2TokenResult result = await client.GetTokenByAuthorizationCodeAsync(authCode, CancellationToken.None);
102+
103+
Assert.NotNull(result);
104+
Assert.Equal(expectedScopes, result.Scopes);
105+
Assert.Equal(expectedAccessToken, result.AccessToken);
106+
Assert.Equal(expectedRefreshToken, result.RefreshToken);
107+
}
108+
109+
[Fact]
110+
public async Task OAuth2Client_GetTokenByRefreshTokenAsync()
111+
{
112+
const string oldAccessToken = "OLD_LET_ME_IN";
113+
const string oldRefreshToken = "OLD_REFRESH_ME";
114+
const string expectedAccessToken = "NEW_LET_ME_IN";
115+
const string expectedRefreshToken = "NEW_REFRESH_ME";
116+
117+
var baseUri = new Uri("https://example.com");
118+
OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
119+
120+
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
121+
122+
string[] expectedScopes = {"read", "write", "delete"};
123+
124+
// Setup an existing access and refresh token
125+
OAuth2Application app = CreateTestApplication();
126+
app.AccessTokens[oldAccessToken] = oldRefreshToken;
127+
app.RefreshTokens[oldRefreshToken] = expectedScopes;
128+
129+
var server = new TestOAuth2Server(endpoints);
130+
server.RegisterApplication(app);
131+
server.Bind(httpHandler);
132+
server.TokenGenerator.AccessTokens.Add(expectedAccessToken);
133+
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);
134+
135+
OAuth2Client client = CreateClient(httpHandler, endpoints);
136+
137+
OAuth2TokenResult result = await client.GetTokenByRefreshTokenAsync(oldRefreshToken, CancellationToken.None);
138+
139+
Assert.NotNull(result);
140+
Assert.Equal(expectedScopes, result.Scopes);
141+
Assert.Equal(expectedAccessToken, result.AccessToken);
142+
Assert.Equal(expectedRefreshToken, result.RefreshToken);
143+
}
144+
145+
[Fact]
146+
public async Task OAuth2Client_GetTokenByDeviceCodeAsync()
147+
{
148+
const string expectedUserCode = "342728";
149+
const string expectedDeviceCode = "ad6498533bf54f4db53e49612a4acfb0";
150+
const string expectedAccessToken = "LET_ME_IN";
151+
const string expectedRefreshToken = "REFRESH_ME";
152+
153+
var baseUri = new Uri("https://example.com");
154+
OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
155+
156+
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
157+
158+
string[] expectedScopes = {"read", "write", "delete"};
159+
160+
var grant = new OAuth2Application.DeviceCodeGrant(expectedUserCode, expectedDeviceCode, expectedScopes);
161+
162+
OAuth2Application app = CreateTestApplication();
163+
app.DeviceGrants.Add(grant);
164+
165+
var server = new TestOAuth2Server(endpoints);
166+
server.RegisterApplication(app);
167+
server.Bind(httpHandler);
168+
server.TokenGenerator.UserCodes.Add(expectedUserCode);
169+
server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode);
170+
server.TokenGenerator.AccessTokens.Add(expectedAccessToken);
171+
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken);
172+
173+
OAuth2Client client = CreateClient(httpHandler, endpoints);
174+
175+
var deviceCodeResult = new OAuth2DeviceCodeResult(expectedDeviceCode, expectedUserCode, null, null);
176+
177+
Task<OAuth2TokenResult> resultTask = client.GetTokenByDeviceCodeAsync(deviceCodeResult, CancellationToken.None);
178+
179+
// Simulate the user taking some time to sign in with the user code
180+
Thread.Sleep(1000);
181+
server.SignInDeviceWithUserCode(expectedUserCode);
182+
183+
OAuth2TokenResult result = await resultTask;
184+
185+
Assert.NotNull(result);
186+
Assert.Equal(expectedScopes, result.Scopes);
187+
Assert.Equal(expectedAccessToken, result.AccessToken);
188+
Assert.Equal(expectedRefreshToken, result.RefreshToken);
189+
}
190+
191+
[Fact]
192+
public async Task OAuth2Client_E2E_InteractiveWebFlowAndRefresh()
193+
{
194+
const string expectedAuthCode = "e78a711d11";
195+
const string expectedAccessToken1 = "LET_ME_IN-1";
196+
const string expectedAccessToken2 = "LET_ME_IN-2";
197+
const string expectedRefreshToken1 = "REFRESH_ME-1";
198+
const string expectedRefreshToken2 = "REFRESH_ME-2";
199+
200+
var baseUri = new Uri("https://example.com");
201+
OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
202+
203+
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
204+
205+
string[] expectedScopes = {"read", "write", "delete"};
206+
207+
OAuth2Application app = CreateTestApplication();
208+
209+
var server = new TestOAuth2Server(endpoints);
210+
server.RegisterApplication(app);
211+
server.Bind(httpHandler);
212+
server.TokenGenerator.AuthCodes.Add(expectedAuthCode);
213+
server.TokenGenerator.AccessTokens.Add(expectedAccessToken1);
214+
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken1);
215+
216+
IOAuth2WebBrowser browser = new TestOAuth2WebBrowser(httpHandler);
217+
218+
OAuth2Client client = CreateClient(httpHandler, endpoints);
219+
220+
string authCode = await client.GetAuthorizationCodeAsync(expectedScopes, browser, CancellationToken.None);
221+
222+
OAuth2TokenResult result1 = await client.GetTokenByAuthorizationCodeAsync(authCode, CancellationToken.None);
223+
224+
Assert.NotNull(result1);
225+
Assert.Equal(expectedScopes, result1.Scopes);
226+
Assert.Equal(expectedAccessToken1, result1.AccessToken);
227+
Assert.Equal(expectedRefreshToken1, result1.RefreshToken);
228+
229+
server.TokenGenerator.AccessTokens.Add(expectedAccessToken2);
230+
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken2);
231+
232+
OAuth2TokenResult result2 = await client.GetTokenByRefreshTokenAsync(result1.RefreshToken, CancellationToken.None);
233+
Assert.NotNull(result2);
234+
Assert.Equal(expectedScopes, result2.Scopes);
235+
Assert.Equal(expectedAccessToken2, result2.AccessToken);
236+
Assert.Equal(expectedRefreshToken2, result2.RefreshToken);
237+
}
238+
239+
[Fact]
240+
public async Task OAuth2Client_E2E_DeviceFlowAndRefresh()
241+
{
242+
const string expectedUserCode = "736998";
243+
const string expectedDeviceCode = "db6558b2a1d649758394ac3c2d9e00b1";
244+
const string expectedAccessToken1 = "LET_ME_IN-1";
245+
const string expectedAccessToken2 = "LET_ME_IN-2";
246+
const string expectedRefreshToken1 = "REFRESH_ME-1";
247+
const string expectedRefreshToken2 = "REFRESH_ME-2";
248+
249+
var baseUri = new Uri("https://example.com");
250+
OAuth2ServerEndpoints endpoints = CreateEndpoints(baseUri);
251+
252+
var httpHandler = new TestHttpMessageHandler {ThrowOnUnexpectedRequest = true};
253+
254+
string[] expectedScopes = {"read", "write", "delete"};
255+
256+
OAuth2Application app = CreateTestApplication();
257+
258+
var server = new TestOAuth2Server(endpoints);
259+
server.RegisterApplication(app);
260+
server.Bind(httpHandler);
261+
server.TokenGenerator.UserCodes.Add(expectedUserCode);
262+
server.TokenGenerator.DeviceCodes.Add(expectedDeviceCode);
263+
server.TokenGenerator.AccessTokens.Add(expectedAccessToken1);
264+
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken1);
265+
266+
OAuth2Client client = CreateClient(httpHandler, endpoints);
267+
268+
OAuth2DeviceCodeResult deviceResult = await client.GetDeviceCodeAsync(expectedScopes, CancellationToken.None);
269+
270+
// Simulate the user taking some time to sign in with the user code
271+
Thread.Sleep(1000);
272+
server.SignInDeviceWithUserCode(deviceResult.UserCode);
273+
274+
OAuth2TokenResult result1 = await client.GetTokenByDeviceCodeAsync(deviceResult, CancellationToken.None);
275+
276+
Assert.NotNull(result1);
277+
Assert.Equal(expectedScopes, result1.Scopes);
278+
Assert.Equal(expectedAccessToken1, result1.AccessToken);
279+
Assert.Equal(expectedRefreshToken1, result1.RefreshToken);
280+
281+
server.TokenGenerator.AccessTokens.Add(expectedAccessToken2);
282+
server.TokenGenerator.RefreshTokens.Add(expectedRefreshToken2);
283+
284+
OAuth2TokenResult result2 = await client.GetTokenByRefreshTokenAsync(result1.RefreshToken, CancellationToken.None);
285+
Assert.NotNull(result2);
286+
Assert.Equal(expectedScopes, result2.Scopes);
287+
Assert.Equal(expectedAccessToken2, result2.AccessToken);
288+
Assert.Equal(expectedRefreshToken2, result2.RefreshToken);
289+
}
290+
291+
#region Helpers
292+
293+
private static OAuth2Application CreateTestApplication() => new OAuth2Application(TestClientId)
294+
{
295+
Secret = TestClientSecret,
296+
RedirectUris = new[] {TestRedirectUri}
297+
};
298+
299+
private static OAuth2Client CreateClient(HttpMessageHandler httpHandler, OAuth2ServerEndpoints endpoints, IOAuth2NonceGenerator generator = null)
300+
{
301+
return new OAuth2Client(new HttpClient(httpHandler), endpoints, TestClientId, TestRedirectUri, TestClientSecret)
302+
{
303+
NonceGenerator = generator
304+
};
305+
}
306+
307+
private static OAuth2ServerEndpoints CreateEndpoints(Uri baseUri)
308+
{
309+
Uri authEndpoint = new Uri(baseUri, "/oauth/v2.0/authorize");
310+
Uri tokenEndpoint = new Uri(baseUri, "/oauth/v2.0/access_token");
311+
Uri deviceEndpoint = new Uri(baseUri, "/oauth/v2.0/authorize_device");
312+
313+
return new OAuth2ServerEndpoints(authEndpoint, tokenEndpoint)
314+
{DeviceAuthorizationEndpoint = deviceEndpoint};
315+
}
316+
317+
#endregion
318+
}
319+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Collections.Generic;
2+
using System.Net.Http;
3+
using System.Threading.Tasks;
4+
5+
namespace Microsoft.Git.CredentialManager
6+
{
7+
public static class HttpContentExtensions
8+
{
9+
public static async Task<IDictionary<string, string>> ReadAsFormContentAsync(this HttpContent content)
10+
{
11+
string str = await content.ReadAsStringAsync();
12+
return UriExtensions.ParseQueryString(str);
13+
}
14+
}
15+
}

src/shared/Microsoft.Git.CredentialManager/UriExtensions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ namespace Microsoft.Git.CredentialManager
99
{
1010
public static class UriExtensions
1111
{
12-
public static IDictionary<string, string> GetQueryParameters(this Uri uri)
12+
public static IDictionary<string, string> ParseQueryString(string queryString)
1313
{
1414
var dict = new Dictionary<string, string>();
1515

16-
string[] queryParts = uri.Query.TrimStart('?').Split('&');
16+
string[] queryParts = queryString.Split('&');
1717
foreach (var queryPart in queryParts)
1818
{
1919
if (string.IsNullOrWhiteSpace(queryPart)) continue;
@@ -34,6 +34,11 @@ public static IDictionary<string, string> GetQueryParameters(this Uri uri)
3434
return dict;
3535
}
3636

37+
public static IDictionary<string, string> GetQueryParameters(this Uri uri)
38+
{
39+
return ParseQueryString(uri.Query.TrimStart('?'));
40+
}
41+
3742
public static bool TryGetUserInfo(this Uri uri, out string userName, out string password)
3843
{
3944
EnsureArgument.NotNull(uri, nameof(uri));

0 commit comments

Comments
 (0)