
What is PASETO?
PASETO *Paseto is everything you love about JOSE (JWT, JWE, JWS) without any of the many design deficits that plague the JOSE…*paseto.io
Why should I move away from JWT?
Both JWT and PASETO are token-based, so they solve the same problem. The trouble is JWT has some rough edges: weak algorithms, tokens that are only Base64 encoded (not encrypted), and forgery that’s easier than you’d like.
If you want to see JWT in action first, here’s my earlier Spring Boot JWT walkthrough.
Spring Boot WebFlux Security JWT Configure Spring Boot WebFlux Security with JWTnutbutterfly.medium.com
PASETO, on the other hand, was designed better from the start: stronger algorithms, and tokens you can’t just decode or decrypt the way you can with JWT. That’s reason enough for me to switch.
How do I start?
Head over to https://paseto.io/, pick your language, and grab a third-party library. Go with the current version or no more than two versions behind. For example, the current PASETO version is v4, so v4 or v3 are both fine, depending on what libraries actually exist for your language.
As of Nov 2023, there’s just one Java library that supports PASETO up to v3, which is https://github.com/nbaars/paseto4j It has a simple, flexible API and it’s easy to work with.
Let’s start
Add the library you picked. I’m using Java, Spring Boot, and a Maven project here.
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 a token object for the app
Let’s make a simple class to carry the token data around the 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 the token service
This is the service I’ll inject into my business logic or REST controllers later. The main things to think about are:
-
How do I create and verify a token?
-
What type of token is it? Local or Public
-
How do I set and check the token’s expiration?
I’m going with a Local token here, so I need a secret key for creating and verifying it.
The nice thing is the payload is freestyle. You can use JSON, XML, plain text, whatever format you like. I’m using JSON.
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, I can test the service with a plain unit test. Let’s add a simple one under src/test.
This test checks the encrypt and decrypt methods of the TokenService we just wrote.
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?
Swap out your old JWT token creation for the new TokenService.encrypt in your REST controller.
Then use TokenService.decrypt to verify the UsernamePasswordAuthenticationToken in your Spring Boot filter.