I assumed that when a JWT is sent to my.Net Core API, the payload is hashed together with the expected signing key and is then checked if the result matches the signing key included in the JWT, and if it does not match it would be rejected as Unauthorised.
The problem is that I created a unit test where I constructed a JWT (like I normally would do for any other token), decrypted it manually, swapped the payload out for another with different user claims, encrypted it back without touching the signing key (so it should no longer match) and header parts of the token, and then sent it to the API, but the API validated it successfully and the request was not rejected.
Here are my Authentication options:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { // validate server that created token ValidateIssuer = true, ValidIssuer = Configuration["Jwt:Issuer"], // check if recipient of token is authorized ValidateAudience = true, ValidAudience = Configuration["Jwt:Audience"], // check if key used to sign token is known to server ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Signingkey"])), RequireSignedTokens = true, // check if token has expired ValidateLifetime = true, RequireExpirationTime = true, ClockSkew = TimeSpan.Zero, // enable decryption of the token using same secret TokenDecryptionKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Secret"])) }; });
And, before anyone asks the obvious, I am using it:
app.UseAuthentication(); // before UseMvc and most other middleware
Here is my unit test (very messy and large but it's more of a spike):
[Test] public void When_Calling_PostAsync_With_Tampered_Payload_And_Original_Header_And_Signature_Then_Should_Return_Unauthorized() { // arrange var originalEncryptedToken = SetupAuthTokenAndApiSender(); var originalDecryptedToken = Decrypt(originalEncryptedToken); var sections = originalDecryptedToken.Split("."); var originalRawHeader = sections[0]; var originalRawPayload = sections[1]; var originalRawSignature = sections[2]; // create another token with a different account ID var otherAccountId = Guid.NewGuid(); Claims.AddClaim("account_id", otherAccountId.ToString()); Claims.AddClaim("anotherKey", "anotherValue"); Claims.AddClaim("stack_id", "2d563cfb-e4a7-4d7e-a8ff-8291b97ee56e"); var otherEncryptedToken = SetupAuthTokenAndApiSender(); // join original header and signature with the payload from the other token and then encrypt the results var forgedEncryptedJwtToken = SwapPayloadAndEncrypt(originalEncryptedToken, originalRawHeader, originalRawSignature, otherEncryptedToken); // decrypt new forged token to assert whether the JWT sections are as expected var forgedDecryptedToken = Decrypt(forgedEncryptedJwtToken); sections = forgedDecryptedToken.Split("."); var forgedRawHeader = sections[0]; var forgedRawPayload = sections[1]; var forgedRawSignature = sections[2]; var innerForgedTokenPayload = JwtPayload.Base64UrlDeserialize(forgedRawPayload); // act TestAudit.AccountId = otherAccountId; var response = TestPostAsync(TestAudit); // assert originalRawHeader.Should().BeEquivalentTo(forgedRawHeader); originalRawPayload.Should().NotBe(forgedRawPayload); originalRawSignature.Should().BeEquivalentTo(forgedRawSignature); innerForgedTokenPayload.Claims.Should().HaveCount(8); innerForgedTokenPayload.Claims.Should().Contain(c => c.Type.Equals("account_id") && c.Value.Equals(TestAudit.AccountId.ToString())); Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", forgedEncryptedJwtToken); response.StatusCode.Should().BeEquivalentTo(HttpStatusCode.Unauthorized); }
So, basically, the header and signature sections are identical to the original token but the payload is very different. The signature signing key does not match the payload anymore so why is the server accepting it? All ideas are appreciated, thank you.
.Net Core Authentication middleware for JWTs - ValidateIssuerSigningKey not working as expected