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.
- Features
- Community
- What is a Paseto Token?
- Installation
- Quickstart
- Keys and Secrets
- JSON Processor
- Learn More
- License
- 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...
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
- Already using JJWT, this library works the same way
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.
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.
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.
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!
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. :)
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.
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.
If you're building a (non-Android) JDK project, you will want to define the following dependencies:
<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 Bouncy Castle, supports all Paseto formats -->
<!--
<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 Lib Sodium for v2 local 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> -->
dependencies {
compile 'dev.paseto:jpaseto-api:0.6.0'
runtime 'dev.paseto:jpaseto-impl:0.6.0',
'dev.paseto:jpaseto-jackson:0.6.0',
// Uncomment the next lines if you want to use Bouncy Castle, supports all Paseto formats
// '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 Lib Sodium for v2 local tokens
// NOTE: this requires the native lib sodium library installed on your system see below
// 'dev.paseto:jpaseto-sodium:0.6.0', // supports v2 local tokens
}
Installation the a native library libsodium is required when creating or parseing "v2.local" tokens, as an alternative to using jpaseto-bouncy-castle
.
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
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-hkdf
and jpaseto-sodium
on a 1.8+ JVM would support all token types.
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.
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!
}
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.
If you want to generate a sufficiently strong SecretKey
for use with "local" tokens, use the
Keys.secretKey()
helper method:
SecretKey key = Keys.secretKey();
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.
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)
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 are a Paseto token's 'payload' and contain the information that the Paseto token creator wishes to present to the token recipient(s).
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 ...
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.
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
}
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.
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.
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.
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.
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.
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.
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 ...
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); // <-----
This project is open-source via the Apache 2.0 License.