// Assert that returned token pair is valid private void checkActionResult(IActionResult actionResult, JwtIssuerOptions jwtIssuerOptions, SymmetricSecurityKey signingKey) { var actionResultType = actionResult.GetType().ToString(); Assert.Equal("Microsoft.AspNetCore.Mvc.OkObjectResult", actionResultType); // I should get back two tokens as json objects var typedActionResult = actionResult as Microsoft.AspNetCore.Mvc.OkObjectResult; Assert.NotNull(typedActionResult); if (typedActionResult == null) { return; } string body = typedActionResult.Value as string; Assert.NotNull(body); if (body == null) { return; } dynamic objResponses = null; bool bDeserialized = true; try { objResponses = JsonConvert.DeserializeObject <List <dynamic> >(body); } catch { bDeserialized = false; } Assert.True(bDeserialized); if (!bDeserialized) { return; } // objResponses should be a list of dynamic objects, the first of // which should be an access token, the second of which should be // a refresh token int count = 0; foreach (var objResponse in objResponses) { count++; } Assert.Equal(2, count); if (count != 2) { return; } var objResponse0 = objResponses[0]; // Access token wrapper var objResponse1 = objResponses[1]; // Refresh token wrapper string sAccessToken = AppUtility.GetDynamicPropertyValueAsString(objResponse0, "access_token"); string sRefreshToken = AppUtility.GetDynamicPropertyValueAsString(objResponse1, "refresh_token"); Int64 iAccessExpiresIn = AppUtility.GetDynamicPropertyValueAsInt64(objResponse0, "expires_in", -1); Int64 iRefreshExpiresIn = AppUtility.GetDynamicPropertyValueAsInt64(objResponse1, "expires_in", -1); // Token strings should not be empty Assert.NotEqual(0, sAccessToken.Length); Assert.NotEqual(0, sRefreshToken.Length); // Expires values should not be null, Refresh value should always be greater than Access value Assert.True(iAccessExpiresIn > 0); Assert.True(iRefreshExpiresIn > 0); Assert.True(iRefreshExpiresIn > iAccessExpiresIn); // I should be able to decode those tokens, using the known secrets // and confirm that the details in the tokens are correct var handler = new JwtSecurityTokenHandler(); var accessTokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtIssuerOptions.Issuer, ValidateAudience = true, ValidAudience = jwtIssuerOptions.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, RequireExpirationTime = true, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(jwtIssuerOptions.AccessClockSkew) }; var refreshTokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = jwtIssuerOptions.Issuer, ValidateAudience = true, ValidAudience = jwtIssuerOptions.Audience, ValidateIssuerSigningKey = true, IssuerSigningKey = signingKey, RequireExpirationTime = true, ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(jwtIssuerOptions.RefreshClockSkew) }; SecurityToken validatedRefreshToken = null; var refreshClaimsPrincipal = handler.ValidateToken(sRefreshToken, refreshTokenValidationParameters, out validatedRefreshToken); Assert.NotNull(validatedRefreshToken); SecurityToken validatedAccessToken = null; var accessClaimsPrincipal = handler.ValidateToken(sAccessToken, accessTokenValidationParameters, out validatedAccessToken); Assert.NotNull(validatedAccessToken); // Cast to JwtSecurityToken var validatedRefreshJwtSecurityToken = validatedRefreshToken as JwtSecurityToken; Assert.NotNull(validatedRefreshJwtSecurityToken); var validatedAccessJwtSecurityToken = validatedAccessToken as JwtSecurityToken; Assert.NotNull(validatedAccessJwtSecurityToken); // AccessClaimsPrincipal should have // Identies collection count of 1 // Identity with Name property of Alice // Identity property with some claims, including role of "User" int countIdentitiesA = 0; foreach (var possibleIdentityA in accessClaimsPrincipal.Identities) { ++countIdentitiesA; } Assert.Equal(1, countIdentitiesA); Assert.Equal("Alice", accessClaimsPrincipal.Identity.Name); int countClaimsA = 0; foreach (var possibleClaimA in accessClaimsPrincipal.Claims) { ++countClaimsA; } Assert.Equal(9, countClaimsA); Assert.True(accessClaimsPrincipal.HasClaim(ClaimTypes.Role, "User")); Assert.True(accessClaimsPrincipal.HasClaim(ClaimTypes.Name, "Alice")); // RefreshClaimsPrincipal should have Name, Guid and IP to validate against database. // Name and Guid may be strings, IP may be empty. int countIdentitiesR = 0; foreach (var possibleIdentityR in refreshClaimsPrincipal.Identities) { ++countIdentitiesR; } Assert.Equal(1, countIdentitiesR); int countClaimsR = 0; foreach (var possibleClaimR in refreshClaimsPrincipal.Claims) { ++countClaimsR; } Assert.Equal(6, countClaimsR); Assert.True(refreshClaimsPrincipal.HasClaim(c => c.Type == JwtController.GUIDKEY)); Assert.True(refreshClaimsPrincipal.HasClaim(JwtController.NAMEKEY, "Alice")); Assert.True(refreshClaimsPrincipal.HasClaim(c => c.Type == JwtController.IPKEY)); }
public void JwtController_RefreshTokenClockSkewWorks() { /////////////////////////////////////////////////////////////////// // Arrange // // Options string secret = "A_KEY_MUST_BE_16_CHARS+"; SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secret.PadRight(16))); SigningCredentials signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); ///////////////////////// int iAccessValidFor = 3; int iRefreshValidFor = 6; // <- Short refresh time for testing UInt32 iRefreshClockSkew = 5; ///////////////////////// var jwtIssuerOptions = new JwtIssuerOptions { Issuer = "TokenIssuer", Audience = "TokenAudience", AccessValidFor = TimeSpan.FromSeconds(iAccessValidFor), RefreshValidFor = TimeSpan.FromSeconds(iRefreshValidFor), AccessClockSkew = 0, RefreshClockSkew = iRefreshClockSkew, Subject = "Alice", SigningCredentials = signingCredentials }; var mockOptions = new Mock <IOptions <JwtIssuerOptions> >(); mockOptions.Setup(mo => mo.Value).Returns(jwtIssuerOptions); // UserManager var fakeUserManager = new FakeUserManager(); // TokenStore var tokenStore = new ExampleTokenStore(); // Configuration var mockConfiguration = new Mock <IConfiguration>(); mockConfiguration.SetupGet(m => m[Constants.SECRET_ENV_VAR]).Returns(secret); // Logger var mockLogger = new Mock <ILogger <JwtController> >(); JwtController controller = new JwtController(mockOptions.Object, fakeUserManager, tokenStore, mockConfiguration.Object, mockLogger.Object); // Credentials LoginCredentials creds = new LoginCredentials { UserName = "******", Password = "******" }; /////////////////////////////////////////////////////////////////// // Act // var issueResult = controller.Issue(creds); var typedActionIssueResult = issueResult.Result as Microsoft.AspNetCore.Mvc.OkObjectResult; Assert.NotNull(typedActionIssueResult); string issueBody = typedActionIssueResult.Value as string; Assert.NotNull(issueBody); dynamic objResponses = null; bool bDeserialized = true; try { objResponses = JsonConvert.DeserializeObject <List <dynamic> >(issueBody); } catch { bDeserialized = false; } Assert.True(bDeserialized); if (!bDeserialized) { return; } // objResponses should be a list of dynamic objects, the first of // which should be an access token, the second of which should be // a refresh token var objResponse0 = objResponses[0]; // Access token wrapper var objResponse1 = objResponses[1]; // Refresh token wrapper string sRefreshToken = AppUtility.GetDynamicPropertyValueAsString(objResponse1, "refresh_token"); // ALLOW TIME TO ELAPSE SO THAT THE REFRESH TOKEN IS STALE IF // ZERO CLOCK SKEW, BUT JUST ABOUT ACCEPTABLE IF CLOCK SKEW IS WORKING System.Threading.Thread.Sleep(1000 * (iRefreshValidFor + (int)iRefreshClockSkew - 2)); // Sleep for at least the timeout time of the refresh token, plus 1 second // Call the Refresh endpoint with the token to see if a new pair of tokens are issued. var refreshResult = controller.Refresh(sRefreshToken); /////////////////////////////////////////////////////////////////// // Assert // var actionResult = refreshResult.Result; Assert.Equal("Microsoft.AspNetCore.Mvc.OkObjectResult", actionResult.GetType().ToString()); }
public void JwtController_CanIssueAndRefreshTokens() { /////////////////////////////////////////////////////////////////// // Arrange // // Options string secret = "A_KEY_MUST_BE_16_CHARS+"; SymmetricSecurityKey signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secret.PadRight(16))); SigningCredentials signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256); var jwtIssuerOptions = new JwtIssuerOptions { Issuer = "TokenIssuer", Audience = "TokenAudience", AccessValidFor = TimeSpan.FromSeconds(10), RefreshValidFor = TimeSpan.FromSeconds(100), AccessClockSkew = 0, RefreshClockSkew = 0, Subject = "Alice", SigningCredentials = signingCredentials }; var mockOptions = new Mock <IOptions <JwtIssuerOptions> >(); mockOptions.Setup(mo => mo.Value).Returns(jwtIssuerOptions); // UserManager var fakeUserManager = new FakeUserManager(); // TokenStore var tokenStore = new ExampleTokenStore(); // Configuration var mockConfiguration = new Mock <IConfiguration>(); mockConfiguration.SetupGet(m => m[Constants.SECRET_ENV_VAR]).Returns(secret); // Logger var mockLogger = new Mock <ILogger <JwtController> >(); JwtController controller = new JwtController(mockOptions.Object, fakeUserManager, tokenStore, mockConfiguration.Object, mockLogger.Object); // Credentials LoginCredentials creds = new LoginCredentials { UserName = "******", Password = "******" }; /////////////////////////////////////////////////////////////////// // Act // var issueResult = controller.Issue(creds); var typedActionIssueResult = issueResult.Result as Microsoft.AspNetCore.Mvc.OkObjectResult; Assert.NotNull(typedActionIssueResult); string issueBody = typedActionIssueResult.Value as string; Assert.NotNull(issueBody); dynamic objResponses = null; bool bDeserialized = true; try { objResponses = JsonConvert.DeserializeObject <List <dynamic> >(issueBody); } catch { bDeserialized = false; } Assert.True(bDeserialized); if (!bDeserialized) { return; } // objResponses should be a list of dynamic objects, the first of // which should be an access token, the second of which should be // a refresh token var objResponse0 = objResponses[0]; // Access token wrapper var objResponse1 = objResponses[1]; // Refresh token wrapper string sRefreshToken = AppUtility.GetDynamicPropertyValueAsString(objResponse1, "refresh_token"); // Call the Refresh endpoint with the token to see if a new pair of tokens are issued. var refreshResult = controller.Refresh(sRefreshToken); checkActionResult(refreshResult.Result, jwtIssuerOptions, signingKey); }