Are you sure your access tokens are really secure?
Chances are high that if you’re building a Web API, you’re relying on access tokens when client applications or services interact with your API in order to perform authentication and authorization. The two most common types of access tokens are opaque (or reference) tokens and JSON Web Tokens (aka JWTs).
These two types of tokens have their pros and cons:
Opaque token | JWT |
+ Better privacy | - JWT can contain sensitive data |
+ Easier to revoke | - Lifetime of a JWT is encoded within |
+ Very small in size, since it’s just a reference | +/- Can be kept small, but can also easily grow large when including lots of claims |
- Additional server requests are needed to check the token’s validity and to retrieve user data | + Self-contained |
- There’s no real defining standard | + Standardized |
Let’s focus on JWT tokens, since this type of token is still the most frequently used type. A JSON Web Token consists of three parts:
eyJhbGciOi...VCJ9 . eyJzdW....MDgxMTMzMX0 . GvHaTrJuFo...s15Ss
header . payload . signature
The first two parts are Base64url-encoded JSON objects, representing the token’s header information and the payload of the token. The third part is a Base64url-encoded array of bytes representing the signature of the first two parts combined.
What’s in the header?
The header of a JWT typically includes the following three properties, but can contain additional information. I’ll circle back to the additional information later, but for now, here are the most common properties:
“typ”: the media type of the token. Sometimes omitted, this value is typically set to “JWT” to indicate that this is a JSON Web Token. For access tokens, you can also see the value “at+jwt” to further specify that the JSON Web Token is an access token.
“alg”: the cryptographic algorithm used to sign the token. Examples are “HS256” for tokens signed using the HMAC SHA-256 algorithm, “RS256” for tokens signed using a private/public RSA key pair and SHA-256, etc.
You can find a complete list in RFC 7518.“kid”: the key ID of the key that was used to sign the JSON Web Token. When validating a JWT signature, the “kid” value is used to look up the correct public key in case multiple JSON Web Keys (JWKs) are available to sign or validate JSON Web Tokens. For example, this is the current list of available JWKs for https://demo.duendesoftware.com: https://demo.duendesoftware.com/.well-known/openid-configuration/jwks
And in the payload?
The payload is a JSON object containing claims. Some claim types are registered and frequently used, or even mandatory, but you can include as many custom claims as you wish. Just remember that every claim adds data to your token, and tokens can become too large to be delivered to client applications depending on the delivery mechanism.
The most commonly used claim types are:
“iss”: the issuer of the JWT. This claim typically points to the service that issues your tokens. In OAuth 2.0 flows, you can most likely use the claim value to find the
/.well-known/openid-configuration
discovery document.“sub”: the subject of the JWT. The value indicates the user or application who can access your API resource server.
“aud”: the audience, or intended recipients for the JWT. The audience value points to your API, to indicate that the JWT was meant to be used to gain access to your API.
“exp”: the expiration time. When this timestamp passes, the JWT is no longer valid for use.
“nbf”: the not-before time. The JWT only becomes valid for use after this timestamp.
“iat”: the issued-at time. This is the timestamp when the JWT was originally created, and can be used to determine the age of the JWT.
“jti”: a unique identifier for the JWT, which can be used to prevent replay attacks.
Validating a JWT
It would be pretty bad if your API would just accept any access token if it contains the correct audience claim value, so we ensure our API only accepts tokens which satisfy a few rules:
The “aud” claim contains (only) values that we expect for our API.
The current date/time falls between the “nbf” and “exp” values. Some leeway can be granted using a clock skew, to accommodate for time drifting between different servers.
The “iss”, or issuer of the token, is a known service which our API trusts.
The “alg”, or signature algorithm, is on our allowed list of cryptographic algorithms.
The signature can be successfully validated using the “kid” and the corresponding public key material of the issuer.
Luckily, there are plenty of open-source libraries out there for a variety of frameworks or programming languages, which you can use to validate JSON Web Tokens. You can find a list of JWT libraries at https://jwt.io/libraries.
But… What if this library contains a vulnerability? Or what if an update breaks one of the critical validation paths? Or if some of the library’s methods work every-so-slightly different when validating tokens, causing some invalid tokens to slip through the maze?
Trust, but verify: test your JWT validation!
The easiest method to add some trust into your API, is by writing tests which validate that various JSON Web Tokens, both valid and invalid, are being properly validated and respectively accepted or rejected by your API. Every time you then update the JWT library you use, you can automatically rerun your tests and verify that the validation still works as expected.
You might wonder what could go wrong. Well, let’s give some examples of tokens that may try to fool the validation logic.
Algorithm? What algorithm?
The “alg” property in the token’s header, again, indicates which cryptographic algorithm is used to sign the token. But this property can also be set to “none”, to indicate that the JWT is not signed at all.
Yes. The following token is a valid JWT:
eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiIxMjM0IiwibmFtZSI6Ildlc2xleSIsImlhdCI6MTczMDgxMTMzMX0.
// The JWT above equals the following:
{
"alg": "none",
"typ": "JWT"
}
.
{
"sub": "1234",
"name": "Wesley",
"iat": 1730811331
}
.
// no signature
This poses a problem, exactly because this is valid as far as RFC 7519 is concerned. Luckily, most validation libraries will allow you to specify which signature algorithms your API allows, and some even disallow “none” from being used by default.
There are some people out there, however, who don’t play nice and think outside the box. These people may try to authenticate against your API using a JWT where the “alg” property is set to “nONe”, or “some-random-value-here”. Which could be enough to fool a poorly written JWT validation library, and yes, some accepted “nONe” as a valid token…
Writing an integration test to catch these invalid or unsupported signature algorithms, can be very easy:
public class SignatureAlgorithmTests(TargetApiWebApplicationFactory factory) : JwtGuardTestBase(factory)
{
[Theory(DisplayName = "When a token uses an unsupported signature algorithm, the API should return a 401 Unauthorized response.")]
[MemberData(nameof(GetDisllowedAlgorithms))]
internal async Task Accessing_AuthorizedUrl_Is_Unauthorized_For_Unsupported_Signature_Algorithms(string signatureAlgorithm)
{
// Arrange
var jwt = await GetJwtAsync(signatureAlgorithm);
Client!.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
// Act
var response = await Client.GetAsync(TestSettings.CurrentTestSettings.TargetUrl);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
private Task<string> GetJwtAsync(string signatureAlgorithm)
{
// Use a JwtBuilder instance to build an access token
return Factory.CreateJwtBuilder()
.WithSignatureAlgorithm(signatureAlgorithm)
.BuildAsync();
}
}
public class JwtBuilder
{
// Most other properties and methods are omitted for brevity...
public Microsoft.IdentityModel.Tokens.SigningCredentials? SigningCredentials { get; private set; }
public string? SignatureAlgorithm { get; private set; }
public JwtBuilder WithSignatureAlgorithm(string signatureAlgorithm)
{
SignatureAlgorithm = signatureAlgorithm;
SigningCredentials = null;
return this;
}
public async Task<string> BuildAsync()
{
if (!string.IsNullOrEmpty(SignatureAlgorithm) &&
(string.Equals(SecurityAlgorithms.None, SignatureAlgorithm, StringComparison.OrdinalIgnoreCase) ||
!TestSettings.KnownSecurityAlgorithms.Contains(SignatureAlgorithm)))
{
// Either using "none" (case-insensitive) or an unknown algorithm. Return an unsigned token.
return BuildJwtHeader().Base64UrlEncode() + "." + BuildJwtPayload().Base64UrlEncode() + ".";
}
// default logic which signs and returns the token
}
}
With a bit of modification, the same test logic can also be used to test that validly signed tokens are rejected if they’re signed using an algorithm which your API doesn’t want to use, like HS256 for example.
But wait, there’s more shenanigans…
Remember when I told you earlier that I would circle back on the claims in a JWT header? Well, there are a few special ones:
“jku”: JWK Set URL, pointing to a resource for a set of JSON Web Keys. While this URL could in theory point to the authority or the issuer of the token, it’s not a requirement! Everyone can create a JWT token, host their public JWK material and add a reference URL to the token header.
“jwk”: using this property, a token include the public JSON Web Key in its header, to allow for full self-validation of the signature. This is very dangerous! Because this means that an attacker can craft a self-signed JSON Web Token and simply include the public JWK in the token’s header!
“x5u”: a URL pointing to a X.509 certificate or certificate chain, which can be used to validate the signature by hosting the public certificate (chain) online. Just like the “jku” property, the URL could in theory live on the same server as the authority or issuer, but this doesn’t need to be the case.
“x5c”: an X.509 certificate or certificate chain, which can be used to self-validate the signature, just like the “jwk” property. Again, very dangerous!
Testing these very specific methods to bypass our API’s security is a bit more challenging, since we need to generate valid signature key material and find a way to host it (for the “jku” and “x5u” test scenarios) externally. But in pseudocode, this is how you would write the tests:
public class ExternalSignatureTests : IntegrationTestBase
{
[Fact]
public async Task RejectExternallySignedToken()
{
// Arrange
var jwt = GetJwt("jwk");
Client!.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", jwt);
// Act
var response = await Client.GetAsync("/secure-api-endpoint");
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
private string GetJwt(string testCase)
{
var signatureAlgorithm = "ES256"
var jwtBuilder = Factory.CreateJwtBuilder()
.WithSignatureAlgorithm(signatureAlgorithm);
var header = jwtBuilder.BuildJwtHeader();
var payload = jwtBuilder.BuildJwtPayload();
var encodedPayload = payload.Base64UrlEncode();
var headerAndPayload = "";
var signature = "";
switch (testCase)
{
case "jwk":
signature = InjectJsonWebKey(signatureAlgorithm, header, encodedPayload, out headerAndPayload);
break;
// other test cases go here...
default:
return jwtBuilder.BuildAsync().GetAwaiter().GetResult();
}
return headerAndPayload + "." + signature;
}
private string InjectJsonWebKey(string signatureAlgorithm, JwtHeader header, string encodedPayload, out string headerAndPayload)
{
var securityKey = SecurityKeyBuilder.CreateSecurityKey(signatureAlgorithm);
var jsonWebKey = JsonWebKeyConverter.ConvertFromSecurityKey(securityKey);
jsonWebKey.Alg = signatureAlgorithm;
jsonWebKey.Use = "sig";
header["jwk"] = jsonWebKey.ToDictionary();
header["kid"] = jsonWebKey.KeyId;
return SignAndReturnJwt(header, encodedPayload, signatureAlgorithm, securityKey, out headerAndPayload);
}
private string SignAndReturnJwt(JwtHeader header, string encodedPayload, string signatureAlgorithm, SecurityKey securityKey, out string headerAndPayload)
{
headerAndPayload = header.Base64UrlEncode() + "." + encodedPayload;
var asciiBytes = Encoding.ASCII.GetBytes(headerAndPayload);
var signatureProvider = CryptoProviderFactory.Default.CreateForSigning(securityKey, signatureAlgorithm);
try
{
var signatureBytes = signatureProvider.Sign(asciiBytes);
return Base64UrlEncoder.Encode(signatureBytes);
}
finally
{
CryptoProviderFactory.Default.ReleaseSignatureProvider(signatureProvider);
}
}
}
Writing these test cases can be very tedious and pose some challenges by themselves. But what if I told you that there’s a solution for that?
Enter JWT Guard
After attending an API security workshop, I got the idea to write some of these test cases for my own API’s, but quickly found that I was repeating myself. So I started working on extracting the test cases in a separate project and made them configurable, to easily apply them to the several API projects.
The end result? JWT Guard.
A template for .NET, allowing you to easily add a JWT test project to your existing API project by these two simple commands:
# You only need to do this once per computer
dotnet new install JWTGuard.Template
# This command runs the JWT Guard template and creates a new JWT test project:
dotnet new jwt-guard --apiProject <relative-path-to-web-api-project>
After adding the test project, all that remains is to configure the TestSettings class in the new test project, so that it know the correct audience for your API and can connect to a secured API “GET” endpoint.
Want to know more? Then visit https://jwtguard.net for the full documentation.
Want to see the code? That’s available over on GitHub.
Feel free to give it a go and let me know what you think!
Subscribe to my newsletter
Read articles from Wesley Cabus directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Wesley Cabus
Wesley Cabus
Wesley is a Coding Architect at Xebia in Belgium, where he helps organizations to build better applications, helps teams to improve their skills and organizes workshops to share his knowledge. He's also a Microsoft Azure MVP, board member of the VISUG meetup in Belgium and speaker at conferences and meetups.