diff --git a/src/Authentication/Authentication.Core/Utilities/JwtHelpers.cs b/src/Authentication/Authentication.Core/Utilities/JwtHelpers.cs index be76c470307..826cd1227d8 100644 --- a/src/Authentication/Authentication.Core/Utilities/JwtHelpers.cs +++ b/src/Authentication/Authentication.Core/Utilities/JwtHelpers.cs @@ -26,23 +26,15 @@ internal static class JwtHelpers internal static void DecodeJWT(string jwToken, IAccount account, ref IAuthContext authContext) { var jwtPayload = DecodeToObject(jwToken); - if (authContext.AuthType == AuthenticationType.UserProvidedAccessToken) + if (authContext.AuthType == AuthenticationType.UserProvidedAccessToken && + jwtPayload != null && + jwtPayload.Exp <= ConvertToUnixTimestamp(DateTime.UtcNow + TimeSpan.FromMinutes(Constants.TokenExpirationBufferInMinutes))) { - if (jwtPayload == null) - { - throw new Exception(string.Format( - CultureInfo.CurrentCulture, - ErrorConstants.Message.InvalidUserProvidedToken, - "AccessToken")); - } - - if (jwtPayload.Exp <= ConvertToUnixTimestamp(DateTime.UtcNow + TimeSpan.FromMinutes(Constants.TokenExpirationBufferInMinutes))) - { - throw new Exception(string.Format( + // Throw exception if access token is expired or is about to expire with a 5 minutes buffer. + throw new Exception(string.Format( CultureInfo.CurrentCulture, ErrorConstants.Message.ExpiredUserProvidedToken, "AccessToken")); - } } authContext.ClientId = jwtPayload?.Appid ?? authContext.ClientId; @@ -74,9 +66,11 @@ internal static T DecodeToObject(string jwtString) internal static JwtContent DecodeJWT(string jwtString) { - // See https://tools.ietf.org/html/rfc7519 - if (string.IsNullOrWhiteSpace(jwtString) || !jwtString.Contains(".") || !jwtString.StartsWith("eyJ")) + if (string.IsNullOrWhiteSpace(jwtString)) throw new ArgumentException("Invalid JSON Web Token (JWT)."); + // See JWT RFC spec: https://tools.ietf.org/html/rfc7519. + if (!jwtString.Contains(".") || !jwtString.StartsWith("eyJ", StringComparison.OrdinalIgnoreCase)) + return null; // Personal account access token are not JWT and cannot be decoded. See https://github.com/microsoftgraph/msgraph-sdk-powershell/issues/2386. var jwtSegments = jwtString.Split('.'); @@ -116,4 +110,4 @@ internal static long ConvertToUnixTimestamp(DateTime time) return (long)timeDiff.TotalSeconds; } } -} \ No newline at end of file +} diff --git a/src/Authentication/Authentication.Test/Helpers/AuthenticationHelpersTests.cs b/src/Authentication/Authentication.Test/Helpers/AuthenticationHelpersTests.cs index 61a06f7a65d..d9dbc26e8be 100644 --- a/src/Authentication/Authentication.Test/Helpers/AuthenticationHelpersTests.cs +++ b/src/Authentication/Authentication.Test/Helpers/AuthenticationHelpersTests.cs @@ -33,8 +33,7 @@ public AuthenticationHelpersTests() public async Task ShouldUseDelegateAuthProviderWhenUserAccessTokenIsProvidedAsync() { // Arrange - string dummyAccessToken = "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiVGVzdCIsIklzc3VlciI6Iklzc3VlciIsIlVzZXJuYW1lIjoiVGVzdCIsImV4cCI6MTY3ODQ4ODgxNiwiaWF0IjoxNjc4NDg4ODE2fQ.hpYypwHAV8H3jb4KuTiLpgLWy9A8H2d9HG7SxJ8Kpn0"; - GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache(Encoding.UTF8.GetBytes(dummyAccessToken)); + GraphSession.Instance.InMemoryTokenCache = new InMemoryTokenCache(Encoding.UTF8.GetBytes(MockConstants.DummyAccessToken)); AuthContext userProvidedAuthContext = new AuthContext { AuthType = AuthenticationType.UserProvidedAccessToken, @@ -48,8 +47,8 @@ public async Task ShouldUseDelegateAuthProviderWhenUserAccessTokenIsProvidedAsyn var accessToken = await authProvider.GetAuthorizationTokenAsync(requestMessage.RequestUri); // Assert - Assert.IsType(authProvider); - Assert.Equal(dummyAccessToken, accessToken); + _ = Assert.IsType(authProvider); + Assert.Equal(MockConstants.DummyAccessToken, accessToken); Assert.Equal(GraphEnvironmentConstants.EnvironmentName.Global, userProvidedAuthContext.Environment); // reset static instance. @@ -72,7 +71,7 @@ public async Task ShouldUseDeviceCodeWhenSpecifiedByUserAsync() TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); // reset static instance. GraphSession.Reset(); @@ -93,7 +92,7 @@ public async Task ShouldUseDeviceCodeWhenFallbackAsync() TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); // reset static instance. GraphSession.Reset(); @@ -113,7 +112,7 @@ public async Task ShouldUseInteractiveProviderWhenDelegatedAsync() TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); // reset static instance. GraphSession.Reset(); @@ -135,7 +134,7 @@ public async Task ShouldUseInteractiveAuthenticationProviderWhenDelegatedContext TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(delegatedAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); // reset static instance. GraphSession.Reset(); @@ -167,13 +166,13 @@ public async Task ShouldUseClientCredentialProviderWhenAppOnlyContextIsProvidedA ContextScope = ContextScope.Process, TenantId = mockAuthRecord.TenantId }; - CreateAndStoreSelfSignedCert(appOnlyAuthContext.CertificateSubjectName); + _ = CreateAndStoreSelfSignedCert(appOnlyAuthContext.CertificateSubjectName); // Act TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); // reset DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateSubjectName); @@ -198,7 +197,7 @@ public async Task ShouldUseInMemoryCertificateWhenProvidedAsync() TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); GraphSession.Reset(); } @@ -209,7 +208,7 @@ public async Task ShouldUseCertNameInsteadOfPassedInCertificateWhenBothAreSpecif // Arrange var dummyCertName = "CN=dummycert"; var inMemoryCertName = "CN=inmemorycert"; - CreateAndStoreSelfSignedCert(dummyCertName); + _ = CreateAndStoreSelfSignedCert(dummyCertName); var inMemoryCertificate = CreateSelfSignedCert(inMemoryCertName); AuthContext appOnlyAuthContext = new AuthContext { @@ -225,7 +224,7 @@ public async Task ShouldUseCertNameInsteadOfPassedInCertificateWhenBothAreSpecif TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); //CleanUp DeleteSelfSignedCertByName(appOnlyAuthContext.CertificateSubjectName); @@ -254,7 +253,7 @@ public async Task ShouldUseCertThumbPrintInsteadOfPassedInCertificateWhenBothAre TokenCredential tokenCredential = await AuthenticationHelpers.GetTokenCredentialAsync(appOnlyAuthContext, default); // Assert - Assert.IsType(tokenCredential); + _ = Assert.IsType(tokenCredential); //CleanUp DeleteSelfSignedCertByThumbprint(appOnlyAuthContext.CertificateThumbprint); diff --git a/src/Authentication/Authentication.Test/Helpers/JwtHelpersTests.cs b/src/Authentication/Authentication.Test/Helpers/JwtHelpersTests.cs new file mode 100644 index 00000000000..653d7e84e85 --- /dev/null +++ b/src/Authentication/Authentication.Test/Helpers/JwtHelpersTests.cs @@ -0,0 +1,68 @@ +using Microsoft.Graph.PowerShell.Authentication; +using Microsoft.Graph.PowerShell.Authentication.Core.Utilities; +using Microsoft.Identity.Client; +using Moq; +using System; +using Xunit; + +namespace Microsoft.Graph.Authentication.Test.Helpers +{ + public class JwtHelpersTests + { + private readonly IAccount _mockIAccount; + public JwtHelpersTests() + { + _mockIAccount = GetIAccountMock(); + } + + [Fact] + public void DecodeJWTStringShouldReturnAuthContextWithClaims() + { + IAuthContext authContext = new AuthContext + { + AuthType = AuthenticationType.Delegated + }; + + JwtHelpers.DecodeJWT(MockConstants.DummyAccessToken, _mockIAccount, ref authContext); + + Assert.Equal("mockAppId", authContext.ClientId); + Assert.Equal("mockTid", authContext.TenantId); + Assert.Equal("upn@contoso.com", authContext.Account); + Assert.Equal(2, authContext.Scopes.Length); + } + + [Fact] + public void DecodeJWTStringShouldReturnNullAuthContextWhenTokenIsNotJWT() + { + IAuthContext authContext = new AuthContext + { + AuthType = AuthenticationType.Delegated + }; + + JwtHelpers.DecodeJWT("EwCQA_NOT_JWT", _mockIAccount, ref authContext); + + Assert.Null(authContext.Scopes); + Assert.Equal("mockUsername", authContext.Account); + } + + [Fact] + public void DecodeJWTStringShouldThrowExceptionWhenTokenIsExpired() + { + IAuthContext authContext = new AuthContext + { + AuthType = AuthenticationType.UserProvidedAccessToken + }; + + _ = Assert.Throws(() => JwtHelpers.DecodeJWT(MockConstants.DummyAccessToken, _mockIAccount, ref authContext)); + } + + private IAccount GetIAccountMock() + { + var accountId = new AccountId("mockId", "mockObjectId", "mockTenantId"); + Mock accountMock = new Mock(); + _ = accountMock.SetupGet(account => account.HomeAccountId).Returns(accountId); + _ = accountMock.SetupGet(account => account.Username).Returns("mockUsername"); + return accountMock.Object; + } + } +} diff --git a/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj b/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj index e11a7533516..8a3d6c004f8 100644 --- a/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj +++ b/src/Authentication/Authentication.Test/Microsoft.Graph.Authentication.Test.csproj @@ -2,12 +2,13 @@ net6.0;net472 false - 2.6.1 + 2.8.0 + all diff --git a/src/Authentication/Authentication.Test/MockConstants.cs b/src/Authentication/Authentication.Test/MockConstants.cs new file mode 100644 index 00000000000..b788e019369 --- /dev/null +++ b/src/Authentication/Authentication.Test/MockConstants.cs @@ -0,0 +1,12 @@ +// ------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information. +// ------------------------------------------------------------------------------ + +namespace Microsoft.Graph.Authentication.Test +{ + internal class MockConstants + { + // This is a dummy access token that is used for testing purposes only. Expired at 2023-10-25T22:23:50.265Z. + internal const string DummyAccessToken = "eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiVGVzdCIsInNjcCI6Im9wZW5pZCBSZXBvcnRzLlJlYWQiLCJ1cG4iOiJ1cG5AY29udG9zby5jb20iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IlRlc3QiLCJhcHBpZCI6Im1vY2tBcHBJZCIsImFwcF9kaXNwbGF5bmFtZSI6Im1vY2tOYW1lIiwiZXhwIjoxNjk4MjcyNjMwLCJpYXQiOjE2OTgyNzI2MzAsInRpZCI6Im1vY2tUaWQifQ.sA7eX8PRxhUTRnXHYZyFB095jszZX75NeIjUae8oGic"; + } +}