Skip to content

Commit c0ef82a

Browse files
zarennerSatu Bailey
authored andcommitted
Use Windows Integrated Auth when possible. Default to long-lived PATs instead of JWTs when interactive is required. (#40)
1 parent 1f6a413 commit c0ef82a

File tree

15 files changed

+302
-43
lines changed

15 files changed

+302
-43
lines changed

Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
33
<PropertyGroup>
4-
<CredentialProviderVersion>0.1.6</CredentialProviderVersion>
4+
<CredentialProviderVersion>0.1.7</CredentialProviderVersion>
55
</PropertyGroup>
66
</Project>

CredentialProvider.Microsoft.Tests/CredentialProviders/Vsts/BearerTokenProviderTests.cs

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public void TestInitialize()
4949
}
5050

5151
[TestMethod]
52-
public async Task Get_WithoutCachedToken_CallsUIFlow()
52+
public async Task Get_WithoutCachedToken_CallsWindowsIntegratedFlow()
5353
{
5454
var source = new Uri("https://example.com/index.json");
5555
var isRetry = false;
@@ -58,13 +58,43 @@ public async Task Get_WithoutCachedToken_CallsUIFlow()
5858

5959
var adalToken = "TestADALToken";
6060
MockCachedToken(null);
61+
MockWindowsIntegratedToken(adalToken);
62+
63+
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
64+
bearerToken.Token.Should().Be(adalToken);
65+
66+
mockAdalTokenProvider
67+
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Once);
68+
mockAdalTokenProvider
69+
.Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()), Times.Once);
70+
mockAdalTokenProvider
71+
.Verify(x => x.AcquireTokenWithUI(It.IsAny<CancellationToken>()), Times.Never);
72+
mockAdalTokenProvider
73+
.Verify(x => x.AcquireTokenWithDeviceFlowAsync(It.IsAny<Func<DeviceCodeResult, Task>>(), It.IsAny<CancellationToken>()), Times.Never);
74+
75+
VerifyAuthority(source);
76+
}
77+
78+
[TestMethod]
79+
public async Task Get_WithoutCachedTokenAndWindowsIntegratedFails_CallsUIFlow()
80+
{
81+
var source = new Uri("https://example.com/index.json");
82+
var isRetry = false;
83+
var isNonInteractive = false;
84+
var canShowDialog = true;
85+
86+
var adalToken = "TestADALToken";
87+
MockCachedToken(null);
88+
MockWindowsIntegratedToken(null);
6189
MockUIToken(adalToken);
6290

6391
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
64-
bearerToken.Should().Be(adalToken);
92+
bearerToken.Token.Should().Be(adalToken);
6593

6694
mockAdalTokenProvider
6795
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Once);
96+
mockAdalTokenProvider
97+
.Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()), Times.Once);
6898
mockAdalTokenProvider
6999
.Verify(x => x.AcquireTokenWithUI(It.IsAny<CancellationToken>()), Times.Once);
70100
mockAdalTokenProvider
@@ -74,7 +104,7 @@ public async Task Get_WithoutCachedToken_CallsUIFlow()
74104
}
75105

76106
[TestMethod]
77-
public async Task Get_WithoutCachedTokenAndUIFlowCanceled_CallsDeviceCodeFlow()
107+
public async Task Get_WithoutCachedTokenAndWindowsIntegratedFailedAndUIFlowCanceled_CallsDeviceCodeFlow()
78108
{
79109
var source = new Uri("https://example.com/index.json");
80110
var isRetry = false;
@@ -83,14 +113,17 @@ public async Task Get_WithoutCachedTokenAndUIFlowCanceled_CallsDeviceCodeFlow()
83113

84114
var adalToken = "TestADALToken";
85115
MockCachedToken(null);
116+
MockWindowsIntegratedToken(null);
86117
MockUIToken(null);
87118
MockDeviceFlowToken(adalToken);
88119

89-
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
90-
bearerToken.Should().Be(adalToken);
120+
var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
121+
bearerTokenResult.Token.Should().Be(adalToken);
91122

92123
mockAdalTokenProvider
93124
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Once);
125+
mockAdalTokenProvider
126+
.Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()), Times.Once);
94127
mockAdalTokenProvider
95128
.Verify(x => x.AcquireTokenWithUI(It.IsAny<CancellationToken>()), Times.Once);
96129
mockAdalTokenProvider
@@ -100,7 +133,7 @@ public async Task Get_WithoutCachedTokenAndUIFlowCanceled_CallsDeviceCodeFlow()
100133
}
101134

102135
[TestMethod]
103-
public async Task Get_WithCachedToken_DoesNotCallFlows()
136+
public async Task Get_WithCachedToken_DoesNotCallAnyFlows()
104137
{
105138
var source = new Uri("https://example.com/index.json");
106139
var isRetry = false;
@@ -110,11 +143,13 @@ public async Task Get_WithCachedToken_DoesNotCallFlows()
110143
var adalToken = "TestADALToken";
111144
MockCachedToken(adalToken);
112145

113-
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
114-
bearerToken.Should().Be(adalToken);
146+
var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
147+
bearerTokenResult.Token.Should().Be(adalToken);
115148

116149
mockAdalTokenProvider
117150
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Once);
151+
mockAdalTokenProvider
152+
.Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()), Times.Never);
118153
mockAdalTokenProvider
119154
.Verify(x => x.AcquireTokenWithUI(It.IsAny<CancellationToken>()), Times.Never);
120155
mockAdalTokenProvider
@@ -133,13 +168,16 @@ public async Task Get_IsRetry_DoesNotQueryCache()
133168

134169
var adalToken = "TestADALToken";
135170
MockCachedToken("OldCachedToken");
171+
MockWindowsIntegratedToken(null);
136172
MockUIToken(adalToken);
137173

138-
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
139-
bearerToken.Should().Be(adalToken);
174+
var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
175+
bearerTokenResult.Token.Should().Be(adalToken);
140176

141177
mockAdalTokenProvider
142178
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Never);
179+
mockAdalTokenProvider
180+
.Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()), Times.Once);
143181
mockAdalTokenProvider
144182
.Verify(x => x.AcquireTokenWithUI(It.IsAny<CancellationToken>()), Times.Once);
145183
mockAdalTokenProvider
@@ -149,7 +187,7 @@ public async Task Get_IsRetry_DoesNotQueryCache()
149187
}
150188

151189
[TestMethod]
152-
public async Task Get_WithoutCachedTokenAndIsNonInteractive_DoesNotCallFlows()
190+
public async Task Get_WithoutCachedTokenAndIsNonInteractive_DoesNotCallInteractiveFlows()
153191
{
154192
var source = new Uri("https://example.com/index.json");
155193
var isRetry = false;
@@ -158,11 +196,13 @@ public async Task Get_WithoutCachedTokenAndIsNonInteractive_DoesNotCallFlows()
158196

159197
MockCachedToken(null);
160198

161-
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
162-
bearerToken.Should().BeNull();
199+
var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
200+
bearerTokenResult.Should().BeNull();
163201

164202
mockAdalTokenProvider
165203
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Once);
204+
mockAdalTokenProvider
205+
.Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()), Times.Once);
166206
mockAdalTokenProvider
167207
.Verify(x => x.AcquireTokenWithUI(It.IsAny<CancellationToken>()), Times.Never);
168208
mockAdalTokenProvider
@@ -172,7 +212,7 @@ public async Task Get_WithoutCachedTokenAndIsNonInteractive_DoesNotCallFlows()
172212
}
173213

174214
[TestMethod]
175-
public async Task Get_WithCachedTokenAndIsNonInteractive_DoesNotCallFlows()
215+
public async Task Get_WithCachedTokenAndIsNonInteractive_DoesNotCallAnyFlows()
176216
{
177217
var source = new Uri("https://example.com/index.json");
178218
var isRetry = false;
@@ -182,11 +222,13 @@ public async Task Get_WithCachedTokenAndIsNonInteractive_DoesNotCallFlows()
182222
var adalToken = "TestADALToken";
183223
MockCachedToken(adalToken);
184224

185-
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
186-
bearerToken.Should().Be(adalToken);
225+
var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
226+
bearerTokenResult.Token.Should().Be(adalToken);
187227

188228
mockAdalTokenProvider
189229
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Once);
230+
mockAdalTokenProvider
231+
.Verify(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()), Times.Never);
190232
mockAdalTokenProvider
191233
.Verify(x => x.AcquireTokenWithUI(It.IsAny<CancellationToken>()), Times.Never);
192234
mockAdalTokenProvider
@@ -203,8 +245,8 @@ public async Task Get_IsRetryIsNonInteractive_ShouldWarn()
203245
var isNonInteractive = true;
204246
var canShowDialog = false;
205247

206-
var bearerToken = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
207-
bearerToken.Should().BeNull();
248+
var bearerTokenResult = await bearerTokenProvider.GetAsync(source, isRetry, isNonInteractive, canShowDialog, cancellationToken);
249+
bearerTokenResult.Should().BeNull();
208250

209251
mockAdalTokenProvider
210252
.Verify(x => x.AcquireTokenSilentlyAsync(It.IsAny<CancellationToken>()), Times.Never);
@@ -226,6 +268,13 @@ private void MockCachedToken(string token)
226268
.Returns(Task.FromResult<IAdalToken>(new AdalToken("Bearer", token)));
227269
}
228270

271+
private void MockWindowsIntegratedToken(string token)
272+
{
273+
mockAdalTokenProvider
274+
.Setup(x => x.AcquireTokenWithWindowsIntegratedAuth(It.IsAny<CancellationToken>()))
275+
.Returns(Task.FromResult<IAdalToken>(new AdalToken("Bearer", token)));
276+
}
277+
229278
private void MockDeviceFlowToken(string token)
230279
{
231280
mockAdalTokenProvider

CredentialProvider.Microsoft/CredentialProviders/Vsts/AdalTokenProvider.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
// Licensed under the MIT license.
44

55
using System;
6+
using System.Security.Claims;
67
using System.Threading;
78
using System.Threading.Tasks;
89
using Microsoft.IdentityModel.Clients.ActiveDirectory;
10+
using NuGetCredentialProvider.Util;
911

1012
namespace NuGetCredentialProvider.CredentialProviders.Vsts
1113
{
@@ -82,5 +84,31 @@ public async Task<IAdalToken> AcquireTokenWithUI(CancellationToken cancellationT
8284
throw;
8385
}
8486
}
87+
88+
public async Task<IAdalToken> AcquireTokenWithWindowsIntegratedAuth(CancellationToken cancellationToken)
89+
{
90+
try
91+
{
92+
string upn = WindowsIntegratedAuthUtils.GetUserPrincipalName();
93+
if (upn == null)
94+
{
95+
return null;
96+
}
97+
98+
var result = await authenticationContext.AcquireTokenAsync(resource, clientId, new UserCredential(upn));
99+
cancellationToken.ThrowIfCancellationRequested();
100+
101+
return new AdalToken(result);
102+
}
103+
catch (AdalServiceException e)
104+
{
105+
if (e.ErrorCode == AdalError.AuthenticationCanceled)
106+
{
107+
return null;
108+
}
109+
110+
throw;
111+
}
112+
}
85113
}
86114
}

CredentialProvider.Microsoft/CredentialProviders/Vsts/BearerTokenProvider.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Runtime.InteropServices;
7+
using System.Security.Claims;
78
using System.Threading;
89
using System.Threading.Tasks;
910
using Microsoft.IdentityModel.Clients.ActiveDirectory;
@@ -33,7 +34,7 @@ public BearerTokenProvider(ILogger logger, IAdalTokenProviderFactory adalTokenPr
3334
this.authUtil = authUtil;
3435
}
3536

36-
public async Task<string> GetAsync(Uri uri, bool isRetry, bool isNonInteractive, bool canShowDialog, CancellationToken cancellationToken)
37+
public async Task<BearerTokenResult> GetAsync(Uri uri, bool isRetry, bool isNonInteractive, bool canShowDialog, CancellationToken cancellationToken)
3738
{
3839
var authority = await authUtil.GetAadAuthorityUriAsync(uri, cancellationToken);
3940
logger.Verbose(string.Format(Resources.AdalUsingAuthority, authority));
@@ -50,14 +51,29 @@ public async Task<string> GetAsync(Uri uri, bool isRetry, bool isNonInteractive,
5051
if (adalToken?.AccessToken != null)
5152
{
5253
logger.Verbose(Resources.AdalAcquireTokenSilentSuccess);
53-
return adalToken.AccessToken;
54+
return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: false);
5455
}
5556
else
5657
{
5758
logger.Verbose(Resources.AdalAcquireTokenSilentFailed);
5859
}
5960
}
6061

62+
// Try Windows Integrated Auth if supported
63+
if (WindowsIntegratedAuthUtils.SupportsWindowsIntegratedAuth())
64+
{
65+
adalToken = await adalTokenProvider.AcquireTokenWithWindowsIntegratedAuth(cancellationToken);
66+
if (adalToken?.AccessToken != null)
67+
{
68+
logger.Verbose(Resources.AdalAcquireTokenWIASuccess);
69+
return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: false);
70+
}
71+
else
72+
{
73+
logger.Verbose(Resources.AdalAcquireTokenWIAFailed);
74+
}
75+
}
76+
6177
// Interactive flows if allowed
6278
if (!isNonInteractive)
6379
{
@@ -69,7 +85,7 @@ public async Task<string> GetAsync(Uri uri, bool isRetry, bool isNonInteractive,
6985

7086
if (adalToken?.AccessToken != null)
7187
{
72-
return adalToken.AccessToken;
88+
return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: true);
7389
}
7490
}
7591
#endif
@@ -88,7 +104,7 @@ public async Task<string> GetAsync(Uri uri, bool isRetry, bool isNonInteractive,
88104
if (adalToken?.AccessToken != null)
89105
{
90106
logger.Verbose(Resources.AdalAcquireTokenDeviceFlowSuccess);
91-
return adalToken.AccessToken;
107+
return new BearerTokenResult(adalToken.AccessToken, obtainedInteractively: true);
92108
}
93109
else
94110
{

CredentialProvider.Microsoft/CredentialProviders/Vsts/IAdalTokenProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public interface IAdalTokenProvider
1616
Task<IAdalToken> AcquireTokenWithDeviceFlowAsync(Func<DeviceCodeResult, Task> deviceCodeHandler, CancellationToken cancellationToken);
1717

1818
Task<IAdalToken> AcquireTokenWithUI(CancellationToken cancellationToken);
19+
20+
Task<IAdalToken> AcquireTokenWithWindowsIntegratedAuth(CancellationToken cancellationToken);
1921
}
2022

2123
public interface IAdalToken

CredentialProvider.Microsoft/CredentialProviders/Vsts/IBearerTokenProvider.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@ namespace NuGetCredentialProvider.CredentialProviders.Vsts
1010
{
1111
public interface IBearerTokenProvider
1212
{
13-
Task<string> GetAsync(Uri uri, bool isRetry, bool isNonInteractive, bool canShowDialog, CancellationToken cancellationToken);
13+
Task<BearerTokenResult> GetAsync(Uri uri, bool isRetry, bool isNonInteractive, bool canShowDialog, CancellationToken cancellationToken);
14+
}
15+
16+
public class BearerTokenResult
17+
{
18+
public BearerTokenResult(string token, bool obtainedInteractively)
19+
{
20+
Token = token;
21+
ObtainedInteractively = obtainedInteractively;
22+
}
23+
24+
public string Token { get; }
25+
26+
public bool ObtainedInteractively { get; }
1427
}
1528
}

0 commit comments

Comments
 (0)