JPaseto - Paseto Library for Java
JPaseto aims to be the easiest to use and understand library for creating and verifying Paseto tokens on the JVM.
JPaseto is a Java implementation based exclusively on the Paseto specification. And is a direct port of JJWT, if you are using JWTs check out that library.
We've also added some convenience extensions that are not yet part of the specification, such as validation of the registered date claims.
The goal of this project is to provide a pure Java implementation of the Paseto specification.
NOTE: "v2.local" tokens currently require the use of a native library "libsodium", a pure java implementation will be available in the future. (see below for more details)
Table of Contents
- Features
- Community
- What is a Paseto Token?
- Installation
- Quickstart
- Keys and Secrets
- JSON Processor
- Learn More
- License
Features
- Fully functional on all JDKs 1.8+
- Automatic security best practices and assertions
- Easy to learn and read API
- Convenient and readable fluent interfaces, great for IDE auto-completion to write code quickly
- Fully RFC-draft specification compliant on all implemented functionality, tested against RFC-specified test vectors
- Convenience enhancements beyond the specification such as
- Claims assertions (requiring specific values)
- Claim POJO marshaling and unmarshaling when using a compatible JSON parser (e.g. Jackson)
- and more...
Differences Between Other Java Paseto Implementations
Why choose this library over the other Java Paseto implementations?
- Fluent API
- Full security audited performed by Paragon Initiative Enterprises
- Available on Maven Central
- Low dependency count(with the exception of libsodium)
- Already using JJWT, this library works the same way
Community
Getting Help
If you have trouble using JPaseto, please first read the documentation on this page before asking questions. We try very hard to ensure JPaseto's documentation is robust, categorized with a table of contents, and up to date for each release.
Questions
If the documentation or the API JavaDoc isn't sufficient, and you either have usability questions or are confused about something, please ask your question here.
If you believe you have found a bug or would like to suggest a feature enhancement, please create a new GitHub issue, however:
Please do not create a GitHub issue to ask a question.
We use GitHub Issues to track actionable work that requires changes to JPaseto's design and/or codebase. If you have a usability question, instead please ask your question here.
Bugs and Feature Requests
If you do not have a usability question and believe you have a legitimate bug or feature request, please do create a new JPaseto issue.
If you feel like you'd like to help fix a bug or implement the new feature yourself, please read the Contributing section next before starting any work.
Contributing
Pull Requests
Simple Pull Requests that fix anything other than JPaseto core code (documentation, JavaDoc, typos, test cases, etc) are always appreciated and have a high likelihood of being merged quickly. Please send them!
However, if you want or feel the need to change JPaseto's functionality or core code, please do not issue a pull request without creating a new JPaseto issue and discussing your desired changes first, before you start working on it.
It would be a shame to reject your earnest and genuinely appreciated pull request if it might not not align with the project's goals, design expectations or planned functionality.
So, please create a new JPaseto issue first to discuss, and then we can see if (or how) a PR is warranted. Thank you!
Help Wanted
If you would like to help, but don't know where to start, please visit the Help Wanted Issues page and pick any of the ones there, and we'll be happy to discuss and answer questions in the issue comments.
If any of those don't appeal to you, no worries! Any help you would like to offer would be appreciated based on the above caveats concerning contributing pull reqeuests. Feel free to discuss or ask questions first if you're not sure. :)
What is a Paseto Token?
Don't know what a Paseto Token is? Read on. Otherwise, jump on down to the Installation section.
Paseto is a means of transmitting information between two parties in a compact, verifiable form.
The bits of information encoded in the body of a Paseto token are called claims
. The expanded form of the Paseto is in a JSON format, so each claim
is a key in the JSON object.
Paseto can be cryptographically signed ("public" tokens) or encrypted with a shared secret ("local" tokens).
This adds a powerful layer of verifiability to the user of Paseto tokens. The receiver has a high degree of confidence that the Paseto token has not been tampered with by verifying the signature, for instance.
The compact representation of a signed Paseto token is a string that has three or four parts, each separated by a .
:
version.purpose.payload.footer
the footer is optional
The version is a string that represents the current version of the protocol. Currently, two versions are specified, which each possess their own ciphersuites. Accepted values: v1, v2.
The purpose is a short string describing the purpose of the token. Accepted values: local, public.
-
local: shared-key authenticated encryption
-
public: public-key digital signatures; not encrypted Any optional data can be appended to the end. This information is NOT encrypted, but it is used in calculating the authentication tag for the payload. It's always base64url-encoded.
-
For local tokens, it's included in the associated data alongside the nonce.
-
For public tokens, it's appended to the message during the actual authentication/signing step, in accordance to our standard format. Thus, if you want unencrypted, but authenticated, tokens, you can simply set your payload to an empty string and your footer to the message you want to authenticate.
Conversely, if you want to support key rotation, you can use the unencrypted footer to store the kid
claim.
There are a number of standard claims, called Registered Claims, see section 6.1 in the specification and sub
(for subject) is one of them.
To compute the signature, you need a secret key to sign it. We'll cover keys later.
Installation
Use your favorite Maven-compatible build tool to pull the dependencies from Maven Central.
The dependencies could differ slightly if you are working with a JDK project.
JDK Projects
If you're building a (non-Android) JDK project, you will want to define the following dependencies:
Maven
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-api</artifactId>
<version>0.6.0</version>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-impl</artifactId>
<version>0.6.0</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-jackson</artifactId>
<version>0.6.0</version>
<scope>runtime</scope>
</dependency>
<!-- Uncomment the next lines if you want to use v1.local tokens -->
<!--
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-bouncy-castle</artifactId>
<version>0.6.0</version>
<scope>runtime</scope>
</dependency> -->
<!-- or this (only 'v1.local' tokens) for smaller dependency (~11 KB for HKDF vs. ~4.3 MB for Bouncy Castle) -->
<!--
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-hkdf</artifactId>
<version>0.6.0</version>
<scope>runtime</scope>
</dependency> -->
<!-- Uncomment the next lines if you want to use v2 tokens -->
<!-- NOTE: this requires the native lib sodium library installed on your system see below -->
<!--
<dependency>
<groupId>dev.paseto</groupId>
<artifactId>jpaseto-sodium</artifactId>
<version>0.6.0</version>
<scope>runtime</scope>
</dependency> -->
Gradle
dependencies {
compile 'dev.paseto:jpaseto-api:0.6.0'
runtime 'dev.paseto:jpaseto-impl:0.6.0',
// Uncomment the next lines if you want to use v1.local tokens
// 'dev.paseto:jpaseto-bouncy-castle:0.6.0',
// or this (only 'v1.local' tokens) for smaller dependency (~11 KB for HKDF vs. ~4.3 MB for Bouncy Castle)
// 'dev.paseto:jpaseto-hkdf:0.6.0',
// Uncomment the next lines if you want to use v2 tokens
// NOTE: this requires the native lib sodium library installed on your system see below
// 'dev.paseto:jpaseto-sodium:0.6.0',
'dev.paseto:jpaseto-jackson:0.6.0'
}
libsodium
Installation the a native library libsodium is required when creating or parseing "v2.local" tokens.
NOTE: public
tokens can be used with the jpaseto-bouncy-castle
dependency or Java 11+. v1.local
tokens require jpaseto-bouncy-castle
or jpaseto-hkdf
.
-
MacOS - Can install libsodium using brew:
brew install libsodium
-
Windows - Download prebuilt binaries
Paseto Format Support
All Paseto formats are supported by JPaseto, the following contains a table of which additional modules are need to support a given token format:
Module (Artifact Id) | Description | v1.local | v1.public | v2.local | v2.public |
---|---|---|---|---|---|
no additional modules * | Java Cryptography Architecture (JCA) | |
|
|
|
jpaseto-bouncy-castle |
Bouncy Castle | |
|
|
|
jpaseto-hkdf |
HKDF, minimal dependency size (~11K), | |
|
|
|
jpaseto-sodium |
https://libsodium.gitbook.io/doc/ | |
|
|
|
* With no additional dependencies v1.public
and v2.public
tokens are supported with via the Java Cryptography Architecture (JCA) API. Generally speaking, without the additional modules listed above v1.public
tokens require Java 11 (and some Java 8 distributions), and v2.public
tokens require Java 15.
NOTE: Multiple implementations can be used together, for example using jpaseto-bouncy-castle
and jpaseto-sodium
on a 1.8+ JVM would support all token types.
Understanding JPaseto Dependencies
Notice the above dependency declarations all have only one compile-time dependency and the rest are declared as runtime dependencies.
This is because JPaseto is designed so you only depend on the APIs that are explicitly designed for you to use in your applications and all other internal implementation details - that can change without warning - are relegated to runtime-only dependencies. This is an extremely important point if you want to ensure stable JPaseto usage and upgrades over time:
JPaseto guarantees semantic versioning compatibility for only the jpaseto-api
.jar.
This is done to benefit you: great care goes into curating the jpaseto-api
.jar and ensuring it contains what you need and remains backwards compatible as much as is possible so you can depend on that safely with compile scope. The runtime jpaseto-impl
.jar strategy affords the JPaseto developers the flexibility to change the internal packages and implementations whenever and however necessary. This helps us implement features, fix bugs, and ship new releases to you more quickly and efficiently.
Quickstart
Most complexity is hidden behind a convenient and readable builder-based fluent interface, great for relying on IDE auto-completion to write code quickly. Here's an example:
import dev.paseto.jpaseto.Pasetos;
import dev.paseto.jpaseto.lang.Keys;
import java.security.SecretKey;
// We need a secret key, so we'll create one just for this example. Usually
// the key would be read from your application configuration instead.
SecretKey key = Keys.secretKey()
String token = Pasetos.V1.LOCAL.builder()
.setSubject("Joe")
.setSharedSecret(key)
.compact();
How easy was that!?
In this case, we are:
- building a Paseto token that will have the registered claim
sub
(subject) set toJoe
. We are then - encrypted the Paseto token using a shared secret with AES-256-CTR algorithm. Finally, we are
- compacting it into its final
String
form.
The resultant token
String looks like this:
v1.local.CuizxAzVIz5bCqAjsZpXXV5mk_WWGHbVxmdF81DORwyYcMLvzoUHUmS_VKvJ1hn5zXyoMkygkEYLM2LM00uBI3G9gXC5VrZCUM-BLZo1q9IDIncAZTxYkE1NUTMz
Now let's verify the Paseto (parsing tokens with invalid signatures or public keys will throw an exception):
assert Pasetos.parserBuilder().setSharedSecret(key).build().parse(token).getClaims().getSubject().equals("Joe");
There are two things going on here. The key
from before is being used to validate the signature of the token. If it fails to verify the token, a PasetoSignatureException
(which extends from PasetoException
) is thrown. Assuming the Paseto token is validated, we parse out the claims and assert that that subject is set to Joe
.
You have to love code one-liners that pack a punch!
But what if parsing or signature validation failed? You can catch PasetoSignatureException
and react accordingly:
try {
Pasetos.parserBuilder().setSharedSecret(key).build().parse(token);
//OK, we can trust this token
} catch (PasetoException e) {
//don't trust the token!
}
Keys and Secrets
Creating Safe Keys
If you don't want to think about key requirements or just want to make your life easier, JPaseto has provided the dev.paseto.jpaseto.lang.Keys
utility class that can generate sufficiently secure keys.
Secret Keys
If you want to generate a sufficiently strong SecretKey
for use with "local" tokens, use the Keys.secretKey()
helper method:
SecretKey key = Keys.secretKey();
Asymmetric Keys
If you want to generate sufficiently strong Elliptic Curve or RSA asymmetric key pairs for use with "public" Ed25519 or RSA algorithms, use the Keys.keyPairFor(Version)
helper method:
KeyPair keyPair = Keys.keyPairFor(Version.V1);
You use the private key (keyPair.getPrivate()
) to create a token and the public key (keyPair.getPublic()
) to parse/verify a token.
Creating a Paseto Token
You create a Paseto token as follows:
- Use one of the
Pasetos.*Builder()
methods to create aPasetoBuilder
instance. - Call
PasetoBuilder
methods to add claims as desired. - Specify the
SecretKey
for "local" or asymmetricPrivateKey
for "public" tokens. - Finally, call the
compact()
method to compact and encrypt/sign, producing the final token.
For example:
String token = Pasetos.V2.LOCAL.builder() // (1)
.setSubject("Bob") // (2)
.setSharedSecret(key) // (3)
.compact(); // (4)
Footer Parameters
A Paseto footer provides metadata about the contents, specifically a kid
when using rotated keys.
If you need to set one or more footer parameters, you can simply call PasetBuilder
footerClaim
one or more times as needed:
String token = Pasetos.V2.PUBLIC.builder()
.setKeyId("myKeyId")
.footerClaim("other", "data")
// ... etc ...
Each time footerClaim
is called, it simply appends the key-value pair to an internal footer
instance, potentially overwriting any existing identically-named key/value pair.
Claims
Claims are a Paseto token's 'payload' and contain the information that the Paseto token creator wishes to present to the token recipient(s).
Standard Claims
The PasetoBuilder
provides convenient setter methods for standard registered Claim names defined in the Paseto specification. They are:
setIssuer
: sets theiss
(Issuer) ClaimsetSubject
: sets thesub
(Subject) ClaimsetAudience
: sets theaud
(Audience) ClaimsetExpiration
: sets theexp
(Expiration Time) ClaimsetNotBefore
: sets thenbf
(Not Before) ClaimsetIssuedAt
: sets theiat
(Issued At) ClaimsetTokenId
: sets thejti
(Token ID) ClaimsetKeyId
: sets thekid
(Key ID) Claim - found in the footer
For example:
Pasetos.V1.PUBLIC.builder()
.setIssuer("me")
.setSubject("Bob")
.setAudience("you")
.setExpiration(expiration) //a java.time.Instant
.setNotBefore(notBefore) //a java.time.Instant
.setIssuedAt(Instant.now()) // for example, now
.setTokenId(UUID.randomUUID()) //just an example id
/// ... etc ...
Custom Claims
If you need to set one or more custom claims that don't match the standard setter method claims shown above, you can simply call PasetoBuilder
claim
one or more times as needed:
Pasetos.V2.PUBLIC.builder()
.claim("hello", "world")
// ... etc ...
Each time claim
is called, it simply appends the key-value pair to an internal Claims
instance, potentially overwriting any existing identically-named key/value pair.
Obviously, you do not need to call claim
for any standard claim name and it is recommended instead to call the standard respective setter method as this enhances readability.
Reading a Paseto Token
You read (parse) a Pasto token as follows:
- Use the
Pasetos.parserBuilder()
method to create aPasetoParserBuilder
instance. - Specify the
SecretKey
(for encrypted "local" tokens) or asymmetricPublicKey
(for signed "public" tokens).1 - Create a reusable and immutable
PasetoParser
. - Finally, call the
parse(String)
method with your tokenString
, producing the original token. - The entire call is wrapped in a try/catch block in case parsing or signature validation fails. We'll cover exceptions and causes for failure later.
1. If you don't know which key to use at the time of parsing, you can look up the key using a KeyResolver
which we'll cover later.
For example:
Paseto paseto;
try {
paseto = Pasetos.parserBuilder() // (1)
.setSharedSecret(key) // (2)
.build() // (3)
.parse(tokenString); // (4)
// we can safely trust the token
} catch (PasetoException ex) { // (5)
// we *cannot* use the token as intended by its creator
}
Verification Key
The most important thing to do when reading a Paseto token is to specify the key to use to verify the token's cryptographic signature. If signature verification fails, the Paseto cannot be safely trusted and should be discarded.
So which key do we use for verification?
-
If the token was encrypted with a
SecretKey
(for "local" tokens), the sameSecretKey
should be specified on thePasetoParserBuilder
. For example:Pasetos.parserBuilder() .setSharedSecret(secretKey) // <---- .build() .parse(tokenString);
-
If the token was signed with a
PrivateKey
(for "public" tokens), that key's correspondingPublicKey
(not thePrivateKey
) should be specified on thePasetoParserBuilder
. For example:Pasetos.parserBuilder() .setPublicKey(publicKey) // <---- publicKey, not privateKey .build() .parse(tokenString);
But you might have noticed something - what if your application doesn't use just a single SecretKey or KeyPair? What if tokens can be created with different SecretKey
s or public/private keys, or a combination of both? How do you know which key to specify if you can't inspect the Paseto token first?
In these cases, you can't call the PasetoParserBuilder
's setSharedSecret
method with a single key - instead, you'll need to use a KeyResolver
, covered next.
Signing Key Resolver
If your application expects tokens that can be signed with different keys, you won't call the setSharedSecret
method. Instead, you'll need to implement the KeyResolver
interface and specify an instance on the PasetoParserBuilder
via the setKeyResolver
method.
For example:
KeyResolver KeyResolver = getMyKeyResolver();
Paseto token = Pasetos.parserBuilder()
.setKeyResolver(KeyResolver) // <----
.build()
.parse(tokenString);
You can simplify things a little by extending from the KeyResolverAdapter
and implementing the resolvePublicKey(Version, Purpose, FooterClaims)
method. For example:
public class MyKeyResolver extends KeyResolverAdapter {
@Override
public PublicKey resolvePublicKey(Version version, Purpose purpose, FooterClaims footer) {
// implement me
}
}
The PasetoParser
will invoke the resolveSigningKey
method after parsing the payload JSON, but before verifying the signature. This allows you to inspect the FooterClaims
argument for any information that can help you look up the Key
to use for verifying that specific token. This is very powerful for applications with more complex security models that might use different keys at different times or for different users or customers.
Which data might you inspect?
The Paseto specification's supported way to do this is to set a kid
(Key ID) field in the footer when the token is being created, for example:
PublicKey signingKey = getSigningKey();
String keyId = getKeyId(signingKey); //any mechanism you have to associate a key with an ID is fine
String token = Pasetos.V1.PUBLIC.builder()
.setKeyId(keyId) // 1
.setPublicKey(signingKey) // 2
.compact();
Then during parsing, your KeyResolver
can inspect the FooterClaims
to get the kid
and then use that value to look up the key from somewhere, like a database. For example:
public class MyKeyResolver extends KeyResolverAdapter {
@Override
public PublicKey resolvePublicKey(Version version, Purpose purpose, FooterClaims footer) {
//inspect the footer, lookup and return the signing key
String keyId = footer.getKeyId(); //or any other field that you need to inspect
PublicKey key = lookupVerificationKey(keyId); //implement me
return key;
}
}
Note that inspecting the footer.getKeyId()
is just the most common approach to look up a key - you could inspect any number of footer fields to determine how to lookup the verification key. It is all based on how the token was created.
Finally remember that for "local" tokens a SecretKey
is used, and for "public" tokens a Public
key is used.
Claim Assertions
You can enforce that the Pasto token you are parsing conforms to expectations that you require and are important for your application.
For example, let's say that you require that the token you are parsing has a specific sub
(subject) value, otherwise you may not trust the token. You can do that by using one of the various require
* methods on the PasetoParserBuilder
:
try {
Pasetos.parserBuilder()
.requireSubject("jsmith")
.setSharedSecret(key)
.build()
.parse(s);
} catch(InvalidClaimException ice) {
// the sub field was missing or did not have a 'jsmith' value
}
If it is important to react to a missing vs an incorrect value, instead of catching InvalidClaimException
, you can catch either MissingClaimException
or IncorrectClaimException
:
try {
Pasetos.parserBuilder().requireSubject("jsmith").setSharedSecret(key).build().parse(s);
} catch(MissingClaimException mce) {
// the parsed token did not have the sub field
} catch(IncorrectClaimException ice) {
// the parsed token had a sub field, but its value was not equal to 'jsmith'
}
You can also require custom fields by using the require(fieldName, requiredFieldValue)
method - for example:
try {
Pasetos.parserBuilder().require("myfield", "myRequiredValue").setSharedSecret(key).build().parse(s);
} catch(InvalidClaimException ice) {
// the 'myfield' field was missing or did not have a 'myRequiredValue' value
}
(or, again, you could catch either MissingClaimException
or IncorrectClaimException
instead).
Please see the PasetoParserBuilder
class and/or JavaDoc for a full list of the various require
* methods you may use for claims assertions.
Accounting for Clock Skew
When parsing a Paseto token, you might find that exp
or nbf
claim assertions fail (throw exceptions) because the clock on the parsing machine is not perfectly in sync with the clock on the machine that created the token. This can cause obvious problems since exp
and nbf
are time-based assertions, and clock times need to be reliably in sync for shared assertions.
You can account for these differences (usually no more than a few minutes) when parsing using the PasetoParserBuilder
's setAllowedClockSkew
. For example:
Pasetos.parserBuilder()
.setAllowedClockSkew(Duration.ofMinutes(3)) // <----
// ... etc ...
.build()
.parse(tokenString);
This ensures that clock differences between the machines can be ignored. Two or three minutes should be more than enough; it would be fairly strange if a production machine's clock was more than 5 minutes difference from most atomic clocks around the world.
Custom Clock Support
If the above setAllowedClockSkew
isn't sufficient for your needs, the timestamps created during parsing for timestamp comparisons can be obtained via a custom time source. Call the PasetoParserBuilder
's setClock
method with an implementation of the java.time.Clock
interface. For example:
Clock clock = new MyClock();
Pasetos.parserBuilder().setClock(myClock) //... etc ...
The PasetoParser
's default Clock
implementation uses Clock.systemUTC()
, as most would expect.
However, supplying your own clock could be useful, especially when writing test cases to guarantee deterministic behavior.
JSON Support
A PasetoBuilder
will serialize the Claims
and FooterClaims
maps (and potentially any Java objects they contain) to JSON with a Serializer<Map<String, ?>>
instance. Similarly, a PasetoParser
will deserialize JSON into the Claims
and FooterClaims
using a Deserializer<Map<String, ?>>
instance.
If you don't explicitly configure a PasetoBuilder
's Serializer
or a PasetoParser
's Deserializer
, JPaseto will automatically attempt to discover and use the following JSON implementation if found in the runtime classpath.
-
Jackson: This will automatically be used if you specify
dev.paseto:jpaseto-jackson
as a project runtime dependency. Jackson supports POJOs as claims with full marshaling/unmarshaling as necessary. -
Gson: This will be used automatically if you specify
dev.paseto:jpaseto-gson
as a project runtime dependency.
If you want to use POJOs as claim values, use the dev.paseto:jpaseto-jackson
dependency (or implement your own Serializer and Deserializer if desired). But beware, Jackson will force a sizable (> 1 MB) dependency to an Android application thus increasing the app download size for mobile users.
Custom JSON Processor
If you don't want to use JPaseto's runtime dependency approach, or just want to customize how JSON serialization and deserialization works, you can implement the Serializer
and Deserializer
interfaces and specify instances of them on the PasetoBuilder
and PasetoParser
respectively. For example:
When creating a Paseto token:
Serializer<Map<String, Object>> serializer = getMySerializer(); //implement me
Pasetos.V2.LOCAL.builder()
.setSerializer(serializer)
// ... etc ...
When reading a Paseto token:
Deserializer<Map<String, Object>> deserializer = getMyDeserializer(); //implement me
Pasetos.parserBuilder()
.setDeserializer(deserializer)
// ... etc ...
Parsing of Custom Claim Types
By default JPaseto will only convert simple claim types: String, Instant, Date, Long, Integer, Short and Byte. If you need to deserialize other types you can configure the JacksonDeserializer
by passing a Map
of claim names to types in through a constructor. For example:
new JacksonDeserializer(Maps.of("user", User.class).build())
This would trigger the value in the user
claim to be deserialized into the custom type of User
. Given the claims body of:
{
"issuer": "https://example.com/issuer",
"user": {
"firstName": "Jill",
"lastName": "Coder"
}
}
The User
object could be retrieved from the user
claim with the following code:
Pasetos.parserBuilder()
.setDeserializer(new JacksonDeserializer(Map.of("user", User.class))) // <-----
.build()
.parse(token)
.getClaims()
.get("user", User.class); // <-----
Learn More
License
This project is open-source via the Apache 2.0 License.