How to Use JWT Securely
Ideas and sample implementations about generation/validation logics and login/logout procedures for secure JWT usage within a Java project.
Join the DZone community and get the full member experience.
Join For FreeIn my articles about Spring Boot Security and LDAP authentication, we implemented JWT as a user information carrier between client and server. You can access those articles from here and here. In this article, we will dig into another concept, usage of JWT securely in a Spring Boot application.
JWT (JSON Web Token) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. You may get detailed information from its official website.
Also, you may find the appropriate JWT library for the programming language you use from here.
Environment Info
For the sake of being on the same page for the code samples being delivered in this article, below are our development environment information;
OS: Windows 10 20H2
IDE: IntelliJ IDEA 2019.3.5 (Ultimate Edition)
Java: 1.8
Spring Boot: 2.5.8
Apache Maven: 3.5.0
Code samples will be delivered under the sections when needed for clarity. Also, full source code in which implementations of the ideas delivered in this article being done can be accessed from my GitHub repo.
Generating JWTs
JWT safety itself depends on how the tokens are implemented and used. Unless best practices and changes in security algorithms are followed, one can easily become the victim of an attack. The following items, which refined from a detailed article, should be observed meticulously in order to start from a quite safe point:
- Clients should obtain JWTs from the server, rather than generating them themselves. Then the server should validate incoming tokens.
- JWTs should be signed with a secret and secrets should be kept 'secret.' Ideally, not being hard-coded in-application, but in a different location. For instance, in database.
- Secrets should not be put into the payload or header elements of a JWT unless it is encrypted.
- Since they are not revocable, tokens should be assigned expiration time as short as possible - minutes or hours at maximum.
- In order to mitigate a situation where two tokens would be created with exactly the same signature, a random ID should be added in the jti claim of a token.
Below is a sample code to which the above rules are applied:
public class JwtUtils {
private static final SignatureAlgorithm HS256 = SignatureAlgorithm.HS256;
private static final String ALG_NAME = HS256.getJcaName();
private static final String TOKEN_ISSUER = "securejwtusage.tanerinal.com";
private JwtUtils() {
}
public static String createJWTToken(String username, String secret, long expiration) {
ZonedDateTime now = ZonedDateTime.now();
return Jwts.builder()
.setSubject(username)
.setAudience(username)
.setIssuedAt(Date.from(now.toInstant()))
.setExpiration(Date.from(now.plusSeconds(expiration).toInstant()))
.signWith(HS256, new SecretKeySpec(DatatypeConverter.parseBase64Binary(secret), ALG_NAME))
.setId(UUID.randomUUID().toString())
.setIssuer(TOKEN_ISSUER)
.compact();
}
...
}
Login
While generating JWTs, it would be a good practice to keep login information (without personal information like passwords, for sure) on the server-side. To explain it more fluently, here, a database table structure for this purpose will be instructed.
Column descriptions are below:
OID: Unique identifier of the record. No direct relation with the JWT structure itself.
CREATED: Creation time of the JWT.
USERNAME: The user (or client) ID to whom the JWT has been generated.
USER_TOKEN: Generated JWT itself.
TOKEN_ACTIVE: 1/0 value that identifies whether the token can be used or not.
Here, users/clients retrieve a JWT from /authentication
API provided by the server. When an authentication request is received, the server authenticates the user against an authentication source like LDAP server (actually, this part is beyond the scope of this article). After successful authentication, the login table above is being queried for any active token for the current user with a query like below:
SELECT lt.*
FROM login_table lt
WHERE lt.username = :current_username AND lt.token_active = 1;
If a record is found; it will be updated to token_active=0
. Then newly generated token will be inserted into this table and be returned to the client.
Token Validation
Tokens should be validated for each and every incoming request against the following information;
- Expiration time: exp, iat, or nbf claims may be used for token expiration validation. As mentioned above, this period should be kept as small as possible.
- Token issuer: Issuer of the token should be validated against a whitelist. The iss claim may be used to capture the issuer of the JWT. An important note here; the value stored in iss claim should match exactly the one in the whitelist.
- Token audience: JWT mechanism has a claim named aud for this purpose. For an ID Token, this claim should contain the client's ID and be verified against a login table which we will cover in later sections. Likewise, for an Access Token, this claim should contain the URL of the API that it is intended for.
- Algorithm: Signed or encrypted tokens contain an alg claim for the name of the algorithm used. The value of this claim should be validated against a whitelist of acceptable algorithms.
- Usability Check: However above checks passed successfully, the token (and maybe the whole request) may be forged and an attacker may try to use it. To prevent the system from this scenario, a check for token usability should be done via an active flag stored in the login table which will be explained in the next section. If the original client logs out properly, the token is being flagged as inactive and may not be used by an attacker within this scenario.
Around the token validation process, there should be an authorization operation but this is beyond the scope of this article. A sample code for token validation from this perspective is delivered below:
public class JwtUtils {
private static final SignatureAlgorithm HS256 = SignatureAlgorithm.HS256;
private static final String ALG_NAME = HS256.getJcaName();
private static final String TOKEN_ISSUER = "securejwtusage.tanerinal.com";
private JwtUtils() {
}
...
public static void verifyToken(String token, String secret, String audience, Optional<AdminPortalUserLogin> userLogin) {
Claims claims = extractAllClaims(token, secret);
Assert.isTrue(StringUtils.equals(claims.getIssuer(), TOKEN_ISSUER), "Invalid token issuer!");
Assert.isTrue(StringUtils.equals(claims.getAudience(), audience), "Invalid token audience!");
Assert.isTrue(userLogin.orElseThrow(() -> new UnauthenticatedException("Logged out token!")).isTokenActive(), "Logged out token!");
}
public static Claims extractAllClaims(String token, String secret) {
return Jwts
.parser()
.setSigningKey(new SecretKeySpec(DatatypeConverter.parseBase64Binary(secret), ALG_NAME))
.parseClaimsJws(token)
.getBody();
}
}
Logout
Logout capability should be offered both by the frontend and by the backend. When a user tries to logout, the frontend should purge their session information. Also, the same should be done at the backend. Since we are talking about a stateless environment for the backend, invalidating the user's token by updating it to token_active=0
state in the login table expressed above is fairly enough for this purpose.
This is achieved via /logout
API within the sample project delivered for this article.
Test
We test our application with Postman. You can find exported Postman collection in my GitHub repo.
First, here is the authentication request and response via /authenticate
API:
The call for /business
API with the token retrieved above and username
:
And last; here is the logout request and response via /logout
api with the token:
Summary
In this article, ideas about the usage of JWT in a secure way have been discussed. Also, Java code samples were delivered in order to make the subject more concrete. Full application code can be accessed via my GitHub repository.
Opinions expressed by DZone contributors are their own.
Comments