Skip to content

Commit da66716

Browse files
committed
Use JsonWebToken type in place of strings for JWTs
1 parent 08b5df0 commit da66716

File tree

9 files changed

+82
-19
lines changed

9 files changed

+82
-19
lines changed

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
using System.Threading.Tasks;
1010
using Microsoft.Git.CredentialManager;
1111
using Microsoft.Git.CredentialManager.Tests.Objects;
12+
using Microsoft.IdentityModel.JsonWebTokens;
1213
using Newtonsoft.Json;
1314
using Xunit;
15+
using static Microsoft.Git.CredentialManager.Tests.TestHelpers;
1416

1517
namespace Microsoft.AzureRepos.Tests
1618
{
@@ -223,7 +225,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_ReturnsPAT()
223225
var orgUri = new Uri("https://dev.azure.com/org/");
224226

225227
const string expectedPat = "PERSONAL-ACCESS-TOKEN";
226-
const string accessToken = "ACCESS-TOKEN";
228+
JsonWebToken accessToken = CreateJwt();
227229
IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};
228230

229231
var identityServiceUri = new Uri("https://identity.example.com/");
@@ -262,7 +264,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_LocSvcReturn
262264
var context = new TestCommandContext();
263265
var orgUri = new Uri("https://dev.azure.com/org/");
264266

265-
const string accessToken = "ACCESS-TOKEN";
267+
JsonWebToken accessToken = CreateJwt();
266268
IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};
267269

268270
var locSvcRequestUri = new Uri(orgUri, ExpectedLocationServicePath);
@@ -282,7 +284,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcRetu
282284
var context = new TestCommandContext();
283285
var orgUri = new Uri("https://dev.azure.com/org/");
284286

285-
const string accessToken = "ACCESS-TOKEN";
287+
JsonWebToken accessToken = CreateJwt();
286288
IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};
287289

288290
var identityServiceUri = new Uri("https://identity.example.com/");
@@ -315,7 +317,7 @@ public async Task AzureDevOpsRestApi_CreatePersonalAccessTokenAsync_IdentSvcRetu
315317
var context = new TestCommandContext();
316318
var orgUri = new Uri("https://dev.azure.com/org/");
317319

318-
const string accessToken = "ACCESS-TOKEN";
320+
JsonWebToken accessToken = CreateJwt();
319321
IEnumerable<string> scopes = new[] {AzureDevOpsConstants.PersonalAccessTokenScopes.ReposWrite};
320322

321323
var identityServiceUri = new Uri("https://identity.example.com/");
@@ -400,12 +402,12 @@ private static void AssertAcceptJson(HttpRequestMessage request)
400402
Assert.Contains(Constants.Http.MimeTypeJson, acceptMimeTypes);
401403
}
402404

403-
private static void AssertBearerToken(HttpRequestMessage request, string bearerToken)
405+
private static void AssertBearerToken(HttpRequestMessage request, JsonWebToken bearerToken)
404406
{
405407
AuthenticationHeaderValue authHeader = request.Headers.Authorization;
406408
Assert.NotNull(authHeader);
407409
Assert.Equal("Bearer", authHeader.Scheme);
408-
Assert.Equal(bearerToken, authHeader.Parameter);
410+
Assert.Equal(bearerToken.EncodedToken, authHeader.Parameter);
409411
}
410412

411413
private static HttpResponseMessage CreateLocationServiceResponse(Uri identityServiceUri)

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.Git.CredentialManager.Tests.Objects;
99
using Moq;
1010
using Xunit;
11+
using static Microsoft.Git.CredentialManager.Tests.TestHelpers;
1112

1213
namespace Microsoft.AzureRepos.Tests
1314
{
@@ -151,7 +152,7 @@ public async Task AzureReposProvider_GetCredentialAsync_ReturnsCredential()
151152
var expectedClientId = AzureDevOpsConstants.AadClientId;
152153
var expectedRedirectUri = AzureDevOpsConstants.AadRedirectUri;
153154
var expectedResource = AzureDevOpsConstants.AadResourceId;
154-
var accessToken = "ACCESS-TOKEN";
155+
var accessToken = CreateJwt("john.doe");
155156
var personalAccessToken = "PERSONAL-ACCESS-TOKEN";
156157

157158
var context = new TestCommandContext();

src/shared/Microsoft.AzureRepos/AzureDevOpsRestApi.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@
99
using System.Text.RegularExpressions;
1010
using System.Threading.Tasks;
1111
using Microsoft.Git.CredentialManager;
12+
using Microsoft.IdentityModel.JsonWebTokens;
1213

1314
namespace Microsoft.AzureRepos
1415
{
1516
public interface IAzureDevOpsRestApi : IDisposable
1617
{
1718
Task<string> GetAuthorityAsync(Uri organizationUri);
18-
Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, string accessToken, IEnumerable<string> scopes);
19+
Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, JsonWebToken accessToken, IEnumerable<string> scopes);
1920
}
2021

2122
public class AzureDevOpsRestApi : IAzureDevOpsRestApi
@@ -84,7 +85,7 @@ public async Task<string> GetAuthorityAsync(Uri organizationUri)
8485
return commonAuthority;
8586
}
8687

87-
public async Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, string accessToken, IEnumerable<string> scopes)
88+
public async Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, JsonWebToken accessToken, IEnumerable<string> scopes)
8889
{
8990
const string sessionTokenUrl = "_apis/token/sessiontokens?api-version=1.0&tokentype=compact";
9091

@@ -93,7 +94,7 @@ public async Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, st
9394
{
9495
throw new ArgumentException($"Provided URI '{organizationUri}' is not a valid Azure DevOps hostname", nameof(organizationUri));
9596
}
96-
EnsureArgument.NotNullOrWhiteSpace(accessToken, nameof(accessToken));
97+
EnsureArgument.NotNull(accessToken, nameof(accessToken));
9798

9899
_context.Trace.WriteLine("Getting Azure DevOps Identity Service endpoint...");
99100
Uri identityServiceUri = await GetIdentityServiceUriAsync(organizationUri, accessToken);
@@ -134,7 +135,7 @@ public async Task<string> CreatePersonalAccessTokenAsync(Uri organizationUri, st
134135

135136
#region Private Methods
136137

137-
private async Task<Uri> GetIdentityServiceUriAsync(Uri organizationUri, string accessToken)
138+
private async Task<Uri> GetIdentityServiceUriAsync(Uri organizationUri, JsonWebToken accessToken)
138139
{
139140
const string locationServicePath = "_apis/ServiceDefinitions/LocationService2/951917AC-A960-4999-8464-E3F0AA25B381";
140141
const string locationServiceQuery = "api-version=1.0";
@@ -273,7 +274,7 @@ private static StringContent CreateAccessTokenRequestJson(Uri organizationUri, I
273274
/// <param name="content">Optional request content.</param>
274275
/// <param name="bearerToken">Optional bearer token for authorization.</param>
275276
/// <returns>HTTP request message.</returns>
276-
private static HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri uri, HttpContent content = null, string bearerToken = null)
277+
private static HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri uri, HttpContent content = null, JsonWebToken bearerToken = null)
277278
{
278279
var request = new HttpRequestMessage(method, uri);
279280

@@ -282,9 +283,9 @@ private static HttpRequestMessage CreateRequestMessage(HttpMethod method, Uri ur
282283
request.Content = content;
283284
}
284285

285-
if (!string.IsNullOrWhiteSpace(bearerToken))
286+
if (bearerToken != null)
286287
{
287-
request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, bearerToken);
288+
request.Headers.Authorization = new AuthenticationHeaderValue(Constants.Http.WwwAuthenticateBearerScheme, bearerToken.EncodedToken);
288289
}
289290

290291
return request;

src/shared/Microsoft.AzureRepos/AzureReposHostProvider.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Threading.Tasks;
66
using Microsoft.Git.CredentialManager;
77
using Microsoft.Git.CredentialManager.Authentication;
8+
using Microsoft.IdentityModel.JsonWebTokens;
89
using KnownGitCfg = Microsoft.Git.CredentialManager.Constants.GitConfiguration;
910

1011
namespace Microsoft.AzureRepos
@@ -73,13 +74,14 @@ public override async Task<ICredential> GenerateCredentialAsync(InputArguments i
7374

7475
// Get an AAD access token for the Azure DevOps SPS
7576
Context.Trace.WriteLine("Getting Azure AD access token...");
76-
string accessToken = await _msAuth.GetAccessTokenAsync(
77+
JsonWebToken accessToken = await _msAuth.GetAccessTokenAsync(
7778
authAuthority,
7879
AzureDevOpsConstants.AadClientId,
7980
AzureDevOpsConstants.AadRedirectUri,
8081
AzureDevOpsConstants.AadResourceId,
8182
remoteUri);
82-
Context.Trace.WriteLineSecrets("Acquired access token. Token='{0}'", new object[] {accessToken});
83+
string atUser = accessToken.GetAzureUserName();
84+
Context.Trace.WriteLineSecrets($"Acquired Azure access token. User='{atUser}' Token='{{0}}'", new object[] {accessToken.EncodedToken});
8385

8486
// Ask the Azure DevOps instance to create a new PAT
8587
var patScopes = new[]

src/shared/Microsoft.Git.CredentialManager/Authentication/MicrosoftAuthentication.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
using System.IO;
66
using System.Reflection;
77
using System.Threading.Tasks;
8+
using Microsoft.IdentityModel.JsonWebTokens;
89

910
namespace Microsoft.Git.CredentialManager.Authentication
1011
{
1112
public interface IMicrosoftAuthentication
1213
{
13-
Task<string> GetAccessTokenAsync(string authority, string clientId, Uri redirectUri, string resource, Uri remoteUri);
14+
Task<JsonWebToken> GetAccessTokenAsync(string authority, string clientId, Uri redirectUri, string resource,
15+
Uri remoteUri);
1416
}
1517

1618
public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthentication
@@ -25,7 +27,8 @@ public class MicrosoftAuthentication : AuthenticationBase, IMicrosoftAuthenticat
2527
public MicrosoftAuthentication(ICommandContext context)
2628
: base(context) {}
2729

28-
public async Task<string> GetAccessTokenAsync(string authority, string clientId, Uri redirectUri, string resource, Uri remoteUri)
30+
public async Task<JsonWebToken> GetAccessTokenAsync(string authority, string clientId, Uri redirectUri,
31+
string resource, Uri remoteUri)
2932
{
3033
string helperPath = FindHelperExecutablePath();
3134

@@ -45,7 +48,7 @@ public async Task<string> GetAccessTokenAsync(string authority, string clientId,
4548
throw new Exception("Missing access token in response");
4649
}
4750

48-
return accessToken;
51+
return new JsonWebToken(accessToken);
4952
}
5053

5154
private string FindHelperExecutablePath()
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using System;
4+
using System.Security.Claims;
5+
using Microsoft.IdentityModel.JsonWebTokens;
6+
7+
namespace Microsoft.Git.CredentialManager
8+
{
9+
public static class JsonWebTokenExtensions
10+
{
11+
public static string GetAzureUserName(this JsonWebToken jwt)
12+
{
13+
string idp = jwt.TryGetClaim("idp", out Claim idpClaim)
14+
? idpClaim.Value.ToLowerInvariant()
15+
: null;
16+
17+
// If the identity provider is AAD (*not* MSA) we should use the UPN claim
18+
if (!StringComparer.OrdinalIgnoreCase.Equals(idp, "live.com") &&
19+
jwt.TryGetClaim("upn", out Claim upnClaim))
20+
{
21+
return upnClaim.Value;
22+
}
23+
24+
// For MSA IDPs or if the UPN claim is missing, we should use the 'email' claim
25+
if (jwt.TryGetClaim("email", out Claim emailClaim))
26+
{
27+
return emailClaim.Value;
28+
}
29+
30+
return null;
31+
}
32+
}
33+
}

src/shared/Microsoft.Git.CredentialManager/Microsoft.Git.CredentialManager.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<ItemGroup>
1818
<PackageReference Include="LibGit2Sharp.NativeBinaries" Version="2.0.278" />
19+
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="5.5.0" />
1920
</ItemGroup>
2021

2122
</Project>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT license.
3+
using Microsoft.IdentityModel.JsonWebTokens;
4+
5+
namespace Microsoft.Git.CredentialManager.Tests
6+
{
7+
public static class TestHelpers
8+
{
9+
public static JsonWebToken CreateJwt(string upn = "test")
10+
{
11+
string header = @"{ 'alg': 'none' }";
12+
string payload = $@"{{ 'upn': '{upn}' }}";
13+
14+
return new JsonWebToken(header, payload);
15+
}
16+
}
17+
}

src/windows/Installer.Windows/Setup.iss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,6 @@ Source: "{#PayloadDir}\Microsoft.AzureRepos.dll"; DestDir:
9090
Source: "{#PayloadDir}\Microsoft.Git.CredentialManager.dll"; DestDir: "{app}"; Flags: ignoreversion
9191
Source: "{#PayloadDir}\Microsoft.Identity.Client.dll"; DestDir: "{app}"; Flags: ignoreversion
9292
Source: "{#PayloadDir}\Microsoft.Identity.Client.Extensions.Msal.dll"; DestDir: "{app}"; Flags: ignoreversion
93+
Source: "{#PayloadDir}\Microsoft.IdentityModel.JsonWebTokens.dll"; DestDir: "{app}"; Flags: ignoreversion
94+
Source: "{#PayloadDir}\Microsoft.IdentityModel.Logging.dll"; DestDir: "{app}"; Flags: ignoreversion
95+
Source: "{#PayloadDir}\Microsoft.IdentityModel.Tokens.dll"; DestDir: "{app}"; Flags: ignoreversion

0 commit comments

Comments
 (0)