Spring Boot: Quick guide to replace JWT with PASETO

Natthapon Pinyo
3 min readNov 16, 2023

What is PASETO?

Why JWT should not being used?

JWT and PASETO are token-based.
JWT has drawbacks about weak algorithm, token are not encrypted just Base64 encode, and possible to trivial forgery.

Here is an example of Spring Boot JWT integration

In the other hand, PASETO come-up with a better design, stronger algorithm, and token can’t decrypted or decoded easily like JWT.
That’s enough reasons to switch from JWT to PASETO.

How to start?

Checkout https://paseto.io/ choose your preferred programming language and 3rd party library. Pick at least current version or lower than two versions. For example, current version of PASETO is v4.
You may pick v4 or v3. Depends on possibility of 3rd party libraries available.

As of Nov 2023, it’s only one Java library that supported PASETO up to v3. Which is https://github.com/nbaars/paseto4j
This library provide a simple API, easy to use and fully flexible for developer.

Let’s start

Add selected library from previous step, this article will demonstrated with Java, Spring Boot, and Maven project.

pom.xml

<dependency>
<groupId>io.github.nbaars</groupId>
<artifactId>paseto4j-version3</artifactId>
<version>2023.1</version>
</dependency>

<!-- optional: only required when your token need an expires date time as Java Instant Date -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

Define Token Object for App

Let create a simple class to be a protocol for exchange token in app.

AppToken.java

import lombok.Data;
import java.time.Instant;

@Data
public class AppToken {

// A unique id from user table in database
private String userId;

// User's role, this attribute is optional
private String role;

// An instant Java date to indicates expiration of token
private Instant expiresDate;

}

Define Token Service

A Service to be injected in Business Logic or REST Controller later.
Main area to focus are

  1. How to create and verify a Token?
  2. Type of token? Local or Public
  3. How to specify and validate Token Expiration

This example will use Local type of token. So, we need to provide a secret key to be used in token creation and verification.

It’s a freestyle token. You can to use any kind token format eg. JSON, XML, RAW text or your own Formatted Text.
This example will use JSON as a format.

application.properties

# any secured 32 characters
app.token.secret=WfvKvfSqJRKkGRe54NvNyH9M4HAyHNwd

# some text to describe token
app.token.footer=POC-PASETO

TokenService.java

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.extern.log4j.Log4j2;
import org.paseto4j.commons.PasetoException;
import org.paseto4j.commons.SecretKey;
import org.paseto4j.commons.Version;
import org.paseto4j.version3.Paseto;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Optional;

@Log4j2
@Service
public class TokenService {

@Value("${app.token.secret}")
String secret;

@Value("${app.token.footer}")
String footer;

public Optional<String> encrypt(AppToken token) {
String payload;
try {
payload = mapper().writeValueAsString(token);
return Optional.of(Paseto.encrypt(key(), payload, footer));
} catch (PasetoException | JsonProcessingException e) {
log.error("Failed to encode token: {}", e.getMessage());
return Optional.empty();
}
}

public Optional<AppToken> decrypt(String token) {
try {
String payload = Paseto.decrypt(key(), token, footer);
AppToken appToken = mapper().readValue(payload, AppToken.class);
if (Instant.now().isAfter(appToken.getExpiresDate())) {
return Optional.empty();
}
return Optional.of(appToken);
} catch (PasetoException | JsonProcessingException e) {
log.error("Failed to decode token: {}", e.getMessage());
return Optional.empty();
}
}

private SecretKey key() {
return new SecretKey(this.secret.getBytes(StandardCharsets.UTF_8), Version.V3);
}

private JsonMapper mapper() {
JsonMapper mapper = new JsonMapper();
mapper.registerModule(new JavaTimeModule());
return mapper;
}

}

Test it

With Spring Boot, we can test a Service with Unit Test.
Let create a simple Test under src/test

This test, will test on encrypt and decrypt method of TokenService from previous step.

TestTokenService.java

import lombok.extern.log4j.Log4j2;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Optional;

@Log4j2
@SpringBootTest
class TestTokenService {

@Autowired
TokenService tokenService;

@Test
void testGoodToken() {
final String userId = "1234";
final String role = "USER";
final Instant expiresDate = Instant.now().plus(5, ChronoUnit.MINUTES);

AppToken appToken = new AppToken();
appToken.setUserId(userId);
appToken.setRole(role);
appToken.setExpiresDate(expiresDate);

Optional<String> optToken = tokenService.encrypt(appToken);
Assertions.assertTrue(optToken.isPresent());
String token = optToken.get();
Assertions.assertNotNull(token);
log.info(token);

Optional<AppToken> optAppToken = tokenService.decrypt(token);
Assertions.assertTrue(optAppToken.isPresent());
AppToken decodedAppToken = optAppToken.get();

Assertions.assertNotNull(decodedAppToken);
Assertions.assertEquals(userId, decodedAppToken.getUserId());
Assertions.assertEquals(role, decodedAppToken.getRole());
Assertions.assertEquals(expiresDate, decodedAppToken.getExpiresDate());
}

@Test
void testBadToken() {
String fakeToken = "v3.local.mu4W-Il_eEMmGFt5Pe5uJrB3Vq3o4XjrdMeUp0grHqf48GgjN_KevFtHwJCEdbTUdiWhL_lQ-B1Qjsl2arf9TRdqw35bwGJgiPn9OAXezvFRhifmRZOTlZB9H_1u-luEzu5Y4SZCcmWtYDKgCt8jUv5KePUBkfWoKtsMmYgoXlSjqIv0bgxEUHG0kYkDUjXwpIc.UE9DLVBBU0VUTw";
Optional<AppToken> optAppToken = tokenService.encrypt(fakeToken);
Assertions.assertTrue(optAppToken.isEmpty());
}

}

What’s next?

Replace your old token creation (JWT) with a new TokenService.encrypt to create token in your REST Controller

Use TokenService.decrypt to verify UsernamePasswordAuthenticationToken in your Spring Boot Filter.

That’s it, happy coding

--

--