Skip to content

Commit 0fceedb

Browse files
authored
Fixes Invalid JWT token using a personal account (#2388)
* fix: Decode valid JWT tokens. * chore: Add unit tests. * chore: Fix typo.
1 parent 856258f commit 0fceedb

File tree

5 files changed

+105
-31
lines changed

5 files changed

+105
-31
lines changed

src/Authentication/Authentication.Core/Utilities/JwtHelpers.cs

Lines changed: 10 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,23 +26,15 @@ internal static class JwtHelpers
2626
internal static void DecodeJWT(string jwToken, IAccount account, ref IAuthContext authContext)
2727
{
2828
var jwtPayload = DecodeToObject<JwtPayload>(jwToken);
29-
if (authContext.AuthType == AuthenticationType.UserProvidedAccessToken)
29+
if (authContext.AuthType == AuthenticationType.UserProvidedAccessToken &&
30+
jwtPayload != null &&
31+
jwtPayload.Exp <= ConvertToUnixTimestamp(DateTime.UtcNow + TimeSpan.FromMinutes(Constants.TokenExpirationBufferInMinutes)))
3032
{
31-
if (jwtPayload == null)
32-
{
33-
throw new Exception(string.Format(
34-
CultureInfo.CurrentCulture,
35-
ErrorConstants.Message.InvalidUserProvidedToken,
36-
"AccessToken"));
37-
}
38-
39-
if (jwtPayload.Exp <= ConvertToUnixTimestamp(DateTime.UtcNow + TimeSpan.FromMinutes(Constants.TokenExpirationBufferInMinutes)))
40-
{
41-
throw new Exception(string.Format(
33+
// Throw exception if access token is expired or is about to expire with a 5 minutes buffer.
34+
throw new Exception(string.Format(
4235
CultureInfo.CurrentCulture,
4336
ErrorConstants.Message.ExpiredUserProvidedToken,
4437
"AccessToken"));
45-
}
4638
}
4739

4840
authContext.ClientId = jwtPayload?.Appid ?? authContext.ClientId;
@@ -74,9 +66,11 @@ internal static T DecodeToObject<T>(string jwtString)
7466

7567
internal static JwtContent DecodeJWT(string jwtString)
7668
{
77-
// See https://tools.ietf.org/html/rfc7519
78-
if (string.IsNullOrWhiteSpace(jwtString) || !jwtString.Contains(".") || !jwtString.StartsWith("eyJ"))
69+
if (string.IsNullOrWhiteSpace(jwtString))
7970
throw new ArgumentException("Invalid JSON Web Token (JWT).");
71+
// See JWT RFC spec: https://tools.ietf.org/html/rfc7519.
72+
if (!jwtString.Contains(".") || !jwtString.StartsWith("eyJ", StringComparison.OrdinalIgnoreCase))
73+
return null; // Personal account access token are not JWT and cannot be decoded. See https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/2386.
8074

8175
var jwtSegments = jwtString.Split('.');
8276

@@ -116,4 +110,4 @@ internal static long ConvertToUnixTimestamp(DateTime time)
116110
return (long)timeDiff.TotalSeconds;
117111
}
118112
}
119-
}
113+
}

src/Authentication/Authentication.Test/Helpers/AuthenticationHelpersTests.cs

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ public AuthenticationHelpersTests()
3333
public async Task ShouldUseDelegateAuthProviderWhenUserAccessTokenIsProvidedAsync()
3434
{
3535
// Arrange
36-
string dummyAccessToken = "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiVGVzdCIsIklzc3VlciI6Iklzc3VlciIsIlVzZXJuYW1lIjoiVGVzdCIsImV4cCI6MTY3ODQ4ODgxNiwiaWF0IjoxNjc4NDg4ODE2fQ.hpYypwHAV8H3jb4KuTiLpgLWy9A8H2d9HG7SxJ8Kpn0";
37-
GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache(Encoding.UTF8.GetBytes(dummyAccessToken));
36+
GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache(Encoding.UTF8.GetBytes(MockConstants.DummyAccessToken));
3837
AuthContext userProvidedAuthContext = new AuthContext
3938
{
4039
AuthType = AuthenticationType.UserProvidedAccessToken,
@@ -48,8 +47,8 @@ public async Task ShouldUseDelegateAuthProviderWhenUserAccessTokenIsProvidedAsyn
4847
var accessToken = await authProvider.GetAuthorizationTokenAsync(requestMessage.RequestUri);
4948

5049
// Assert
51-
Assert.IsType<AzureIdentityAccessTokenProvider>(authProvider);
52-
Assert.Equal(dummyAccessToken, accessToken);
50+
_ = Assert.IsType<AzureIdentityAccessTokenProvider>(authProvider);
51+
Assert.Equal(MockConstants.DummyAccessToken, accessToken);
5352
Assert.Equal(GraphEnvironmentConstants.EnvironmentName.Global, userProvidedAuthContext.Environment);
5453

5554
// reset static instance.
@@ -72,7 +71,7 @@ public async Task ShouldUseDeviceCodeWhenSpecifiedByUserAsync()
7271
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default);
7372

7473
// Assert
75-
Assert.IsType<DeviceCodeCredential>(tokenCredential);
74+
_ = Assert.IsType<DeviceCodeCredential>(tokenCredential);
7675

7776
// reset static instance.
7877
GraphSession.Reset();
@@ -93,7 +92,7 @@ public async Task ShouldUseDeviceCodeWhenFallbackAsync()
9392
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default);
9493

9594
// Assert
96-
Assert.IsType<DeviceCodeCredential>(tokenCredential);
95+
_ = Assert.IsType<DeviceCodeCredential>(tokenCredential);
9796

9897
// reset static instance.
9998
GraphSession.Reset();
@@ -113,7 +112,7 @@ public async Task ShouldUseInteractiveProviderWhenDelegatedAsync()
113112
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default);
114113

115114
// Assert
116-
Assert.IsType<InteractiveBrowserCredential>(tokenCredential);
115+
_ = Assert.IsType<InteractiveBrowserCredential>(tokenCredential);
117116

118117
// reset static instance.
119118
GraphSession.Reset();
@@ -135,7 +134,7 @@ public async Task ShouldUseInteractiveAuthenticationProviderWhenDelegatedContext
135134
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default);
136135

137136
// Assert
138-
Assert.IsType<InteractiveBrowserCredential>(tokenCredential);
137+
_ = Assert.IsType<InteractiveBrowserCredential>(tokenCredential);
139138

140139
// reset static instance.
141140
GraphSession.Reset();
@@ -167,13 +166,13 @@ public async Task ShouldUseClientCredentialProviderWhenAppOnlyContextIsProvidedA
167166
ContextScope = ContextScope.Process,
168167
TenantId = mockAuthRecord.TenantId
169168
};
170-
CreateAndStoreSelfSignedCert(appOnlyAuthContext.CertificateSubjectName);
169+
_ = CreateAndStoreSelfSignedCert(appOnlyAuthContext.CertificateSubjectName);
171170

172171
// Act
173172
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default);
174173

175174
// Assert
176-
Assert.IsType<ClientCertificateCredential>(tokenCredential);
175+
_ = Assert.IsType<ClientCertificateCredential>(tokenCredential);
177176

178177
// reset
179178
DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateSubjectName);
@@ -198,7 +197,7 @@ public async Task ShouldUseInMemoryCertificateWhenProvidedAsync()
198197
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default);
199198

200199
// Assert
201-
Assert.IsType<ClientCertificateCredential>(tokenCredential);
200+
_ = Assert.IsType<ClientCertificateCredential>(tokenCredential);
202201

203202
GraphSession.Reset();
204203
}
@@ -209,7 +208,7 @@ public async Task ShouldUseCertNameInsteadOfPassedInCertificateWhenBothAreSpecif
209208
// Arrange
210209
var dummyCertName = "CN=dummycert";
211210
var inMemoryCertName = "CN=inmemorycert";
212-
CreateAndStoreSelfSignedCert(dummyCertName);
211+
_ = CreateAndStoreSelfSignedCert(dummyCertName);
213212
var inMemoryCertificate = CreateSelfSignedCert(inMemoryCertName);
214213
AuthContext appOnlyAuthContext = new AuthContext
215214
{
@@ -225,7 +224,7 @@ public async Task ShouldUseCertNameInsteadOfPassedInCertificateWhenBothAreSpecif
225224
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default);
226225

227226
// Assert
228-
Assert.IsType<ClientCertificateCredential>(tokenCredential);
227+
_ = Assert.IsType<ClientCertificateCredential>(tokenCredential);
229228

230229
//CleanUp
231230
DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateSubjectName);
@@ -254,7 +253,7 @@ public async Task ShouldUseCertThumbPrintInsteadOfPassedInCertificateWhenBothAre
254253
TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default);
255254

256255
// Assert
257-
Assert.IsType<ClientCertificateCredential>(tokenCredential);
256+
_ = Assert.IsType<ClientCertificateCredential>(tokenCredential);
258257

259258
//CleanUp
260259
DeleteSelfSignedCertByThumbprint(appOnlyAuthContext.CertificateThumbprint);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using Microsoft.Graph.PowerShell.Authentication;
2+
using Microsoft.Graph.PowerShell.Authentication.Core.Utilities;
3+
using Microsoft.Identity.Client;
4+
using Moq;
5+
using System;
6+
using Xunit;
7+
8+
namespace Microsoft.Graph.Authentication.Test.Helpers
9+
{
10+
public class JwtHelpersTests
11+
{
12+
private readonly IAccount _mockIAccount;
13+
public JwtHelpersTests()
14+
{
15+
_mockIAccount = GetIAccountMock();
16+
}
17+
18+
[Fact]
19+
public void DecodeJWTStringShouldReturnAuthContextWithClaims()
20+
{
21+
IAuthContext authContext = new AuthContext
22+
{
23+
AuthType = AuthenticationType.Delegated
24+
};
25+
26+
JwtHelpers.DecodeJWT(MockConstants.DummyAccessToken, _mockIAccount, ref authContext);
27+
28+
Assert.Equal("mockAppId", authContext.ClientId);
29+
Assert.Equal("mockTid", authContext.TenantId);
30+
Assert.Equal("[email protected]", authContext.Account);
31+
Assert.Equal(2, authContext.Scopes.Length);
32+
}
33+
34+
[Fact]
35+
public void DecodeJWTStringShouldReturnNullAuthContextWhenTokenIsNotJWT()
36+
{
37+
IAuthContext authContext = new AuthContext
38+
{
39+
AuthType = AuthenticationType.Delegated
40+
};
41+
42+
JwtHelpers.DecodeJWT("EwCQA_NOT_JWT", _mockIAccount, ref authContext);
43+
44+
Assert.Null(authContext.Scopes);
45+
Assert.Equal("mockUsername", authContext.Account);
46+
}
47+
48+
[Fact]
49+
public void DecodeJWTStringShouldThrowExceptionWhenTokenIsExpired()
50+
{
51+
IAuthContext authContext = new AuthContext
52+
{
53+
AuthType = AuthenticationType.UserProvidedAccessToken
54+
};
55+
56+
_ = Assert.Throws<Exception>(() => JwtHelpers.DecodeJWT(MockConstants.DummyAccessToken, _mockIAccount, ref authContext));
57+
}
58+
59+
private IAccount GetIAccountMock()
60+
{
61+
var accountId = new AccountId("mockId", "mockObjectId", "mockTenantId");
62+
Mock<IAccount> accountMock = new Mock<IAccount>();
63+
_ = accountMock.SetupGet(account => account.HomeAccountId).Returns(accountId);
64+
_ = accountMock.SetupGet(account => account.Username).Returns("mockUsername");
65+
return accountMock.Object;
66+
}
67+
}
68+
}

src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22
<PropertyGroup>
33
<TargetFrameworks>net6.0;net472</TargetFrameworks>
44
<IsPackable>false</IsPackable>
5-
<Version>2.6.1</Version>
5+
<Version>2.8.0</Version>
66
</PropertyGroup>
77
<ItemGroup>
88
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
99
<!-- As described in this post https://devblogs.microsoft.com/powershell/depending-on-the-right-powershell-nuget-package-in-your-net-project, reference the SDK for dotnetcore-->
1010
<PackageReference Include="Microsoft.PowerShell.SDK" Version="7.2.2" PrivateAssets="all" Condition="'$(TargetFramework)' == 'net6.0'" />
11+
<PackageReference Include="Moq" Version="4.20.69" />
1112
<PackageReference Include="xunit" Version="2.4.2" />
1213
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
1314
<PrivateAssets>all</PrivateAssets>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
namespace Microsoft.Graph.Authentication.Test
6+
{
7+
internal class MockConstants
8+
{
9+
// This is a dummy access token that is used for testing purposes only. Expired at 2023-10-25T22:23:50.265Z.
10+
internal const string DummyAccessToken = "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiVGVzdCIsInNjcCI6Im9wZW5pZCBSZXBvcnRzLlJlYWQiLCJ1cG4iOiJ1cG5AY29udG9zby5jb20iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IlRlc3QiLCJhcHBpZCI6Im1vY2tBcHBJZCIsImFwcF9kaXNwbGF5bmFtZSI6Im1vY2tOYW1lIiwiZXhwIjoxNjk4MjcyNjMwLCJpYXQiOjE2OTgyNzI2MzAsInRpZCI6Im1vY2tUaWQifQ.sA7eX8PRxhUTRnXHYZyFB095jszZX75NeIjUae8oGic";
11+
}
12+
}

0 commit comments

Comments
 (0)