When it comes to securing web applications, there are various options for authentication and authorization. Two popular choices are PASETO and JWT.
While JWT has been widely utilized for many years, PASETO is a relatively new technology that has gained traction due to its enhanced security features.
PASETO (Platform-Agnostic Security Tokens) is both a specification and a reference implementation for secure stateless tokens. It serves as a highly secure alternative to JWT.
Understanding Token-Based Authentication
The typical flow for authenticating a user in a secured API involves the following steps:
- The user provides their username and password for authentication.
- Upon successful verification, the API returns an access token (either JWT or PASETO).
- The access token is included in the Authorization header when making requests to protected endpoints.
- The API server validates the token and responds with the appropriate secured data if the access is valid.
Understanding JWT and Its Limitations
JWT consists of three parts:
- The header, which contains the token's signing algorithm.
- The payload, which holds information about the authenticated user and additional data. The server can customize this part of the payload.
- The signature, generated by the server using a private key. This signature enables the server to verify the authenticity of the JWT during the validation process.
![JWT Demo](/assets/img/articles/2023-05-16-An-overview-of-PASETO-Token-Based-Authentication/jwt.webp
Photo by Wallarm
While JWT provides flexibility in choosing the digital signature algorithm and verification implementation, this flexibility also introduces vulnerabilities.
There are multiple algorithms to choose from, some of which may be weak and susceptible to attacks, such as ECDSA (vulnerable to invalid-curve attacks) or RSA PKCSv1.5 (vulnerable to padding oracle attacks).
JWT implementations are also prone to errors, which can result in security vulnerabilities like broken JWT validation.
When used correctly, JWT can be a reliable and flexible authentication system. However, caution must be exercised to avoid exposing the server to potential attacks.
In contrast, PASETO addresses these issues by simplifying the implementation process.
PASETO: The Solution
While JWT offers implementation flexibility, PASETO takes a more rigid approach. However, this rigidity helps prevent implementation errors and misuse.
PASETO is designed to be user-friendly and offers higher cryptographic resilience compared to JWT.
When using PASETO, the user only needs to configure two settings:
- The PASETO version (
v1
,v2
,v3
, orv4
) specified via theversion
field of the token. - Whether encryption and decryption should be symmetric or asymmetric, indicated by the
purpose
field of the token.
That's all it takes.
PASETO Token Structure
Similar to JSON Web Tokens (JWTs), PASETO tokens are composed of dot-separated base64url encoded data organized in the following format:
version.purpose.payload.footer
version
: Allows for incremental improvements to the token format. The current versions are "v1," "v2," "v3," and "v4."v1
: Utilizes strong cryptographic primitives that are widely available today.v2
: Utilizes newer and stronger cryptographic primitives, but is supported by fewer cryptographic libraries.
purpose
: A concise string that describes the token format as either "local" or "public."local
: The token's payload is encrypted and can only be accessed by parties possessing the shared key.public
: The payload is NOT encrypted; instead, it is signed and verified using a public key.
payload
: Encoded data with a format specific to the token's version and purpose.footer
(optional): Unencrypted JSON, typically used to store the ID of a public key for token validation.
All PASETO token formats are tamperproof, ensuring that any modifications to the token result in failed validation.
Local Tokens (Symmetric Encryption)
Local tokens are always symmetrically encrypted using a shared secret key. This means that the contents of a local PASETO token cannot be viewed without the correct secret key.
![Symmetric encryption](/assets/img/articles/2023-05-16-An-overview-of-PASETO-Token-Based-Authentication/symmetric.webp
Here is an example of a local PASETO token, including its decoded payload, optional footer, and the signing key used to sign the information.
Public Tokens (Asymmetric Encryption)
Public PASETO tokens are suitable for scenarios where it is not safe to share a secret key with all involved parties.
Public tokens are not encrypted but are digitally signed. This means that if an attacker obtains a public PASETO token, they can view its contents but cannot modify it without detection due to the digital signatures used in PASETO tokens.
If an attempt is made to verify a maliciously modified public PASETO token, an error will occur.
![Asymmetric encryption](/assets/img/articles/2023-05-16-An-overview-of-PASETO-Token-Based-Authentication/asymmetric.webp
Here is an example of a public PASETO token, including its decoded payload, optional footer, and the public and private keys used to sign the information.
Versions
Each PASETO version introduces improvements over its predecessor. To implement the PASETO specification correctly, refer to the details provided for each version:
https://github.com/paseto-standard/paseto-spec/tree/master/docs/01-Protocol-Versions
Libraries
You can find all the libraries implementing PASETO for all the popular languages along with the supported versions here:
Implementation using the Ruby library
We will follow an example of usage of the Ruby library implementing PASETO:
https://github.com/bannable/paseto
We need to first install the gem.
gem 'ruby-paseto'
gem 'rbnacl', '~> 7.1.1' # optional - only if PASETO version 4 will be used
Symmetric encryption (local)
require 'paseto'
####################
#### ENCRYPTION ####
####################
# typically, this shared 32 bytes key is stored in both the authentication server
# and the client server
shared_secret_key = SecureRandom.bytes(32)
# initialize the PASETO encrypter/decrypter
crypt = Paseto::V4::Local.new(ikm: shared_secret_key) # version: v4 / purpose: local
# payload in plain text
claims = { "company" => "monstarlab" }
footer = { "viewable" => "yes" }
# encode the payload and get the PASETO
encrypted_token = crypt.encode(claims, footer: JSON.dump(footer))
# => "v4.local.E1Y_KQ6Ek8lSOKrJ6kI1YjWXfAKJ0OEcdhUPywznjBjK5SGDUr4-6rbaZk-CIM_mdQgQHGPj8yAWQswktkCe_Sm_Nj9eEfDxNBFeAC2KsgqFCjF07VJo5ail0jnSTNM0-PekMytJleea8OvNkKdoLs4GKAsZTTJ_-DEMmOMyVlddlmaWoVwnF2JkpjBzFRO7d6PlIIY29rQWOXSvZxoLEqkE5XJvHpFs4NTuCHnF4Pko10X_sgHCPkTGXkWNDg.eyJ2aWV3YWJsZSI6InllcyJ9"
####################
#### DECRYPTION ####
####################
# typically, this shared 32 bytes key is stored in both the authentication server
# and the client server
shared_secret_key = SecureRandom.bytes(32)
# initialize the PASETO encrypter/decrypter
crypt = Paseto::V4::Local.new(ikm: shared_secret_key) # version: v4 / purpose: local
encrypted_token = "v4.local.E1Y_KQ6Ek8lSOKrJ6kI1YjWXfAKJ0OEcdhUPywznjBjK5SGDUr4-6rbaZk-CIM_mdQgQHGPj8yAWQswktkCe_Sm_Nj9eEfDxNBFeAC2KsgqFCjF07VJo5ail0jnSTNM0-PekMytJleea8OvNkKdoLs4GKAsZTTJ_-DEMmOMyVlddlmaWoVwnF2JkpjBzFRO7d6PlIIY29rQWOXSvZxoLEqkE5XJvHpFs4NTuCHnF4Pko10X_sgHCPkTGXkWNDg.eyJ2aWV3YWJsZSI6InllcyJ9"
# note that the last part of the token `eyJ2aWV3YWJsZSI6InllcyJ9` is only a base64 encoded string
# meaning anyone can see its contents
Base64.decode64("eyJ2aWV3YWJsZSI6InllcyJ9")
# => "{\"viewable\":\"yes\"}"
# decoding the token and get the payload
crypt.decode(encrypted_token)
# => <Paseto::Result
# claims={
# "exp"=>"2023-05-10T11:18:41+09:00",
# "iat"=>"2023-05-10T10:18:41+09:00",
# "nbf"=>"2023-05-10T10:18:41+09:00",
# "company"=>"monstarlab"},
# footer={"viewable"=>"yes"}
# >
# if the token has been maliciously modified, an error will be raised
encrypted_token[-1] = "M"
crypt.decode(encrypted_token)
# Paseto::InvalidAuthenticator: Paseto::InvalidAuthenticator
# from /Users/tony_duong/.rvm/gems/ruby-3.1.3/gems/ruby-paseto-0.1.2/lib/paseto/symmetric_key.rb:53:in `decrypt'
Usage of asymmetric encryption (public)
We first start by generating a public/private key pair.
ssh-keygen
# Output: public key and private key
require 'paseto'
####################
#### ENCRYPTION ####
####################
# initialize the PASETO encrypter/decrypter
pem = File.read('private_key')
signer = Paseto::V4::Public.new(pem)
# payload in plain text
claims = { "company" => "monstarlab" }
footer = { "viewable" => "yes" }
# encode the payload and get the PASETO
signed_token = signer.encode(claims, footer: footer)
# => "v4.public.eyJleHAiOiIyMDIzLTA1LTEwVDExOjQ2OjA0KzA5OjAwIiwiaWF0IjoiMjAyMy0wNS0xMFQxMDo0NjowNCswOTowMCIsIm5iZiI6IjIwMjMtMDUtMTBUMTA6NDY6MDQrMDk6MDAiLCJjb21wYW55IjoibW9uc3RhcmxhYiJ9taKQPCARAZHv85xk7yaWPDeWHaHt981eHmoiYIrIcA-monnIbMax2EDxIjObgr6qLLuYzAH4BK5N6q0TJANeBg.eyJ2aWV3YWJsZSI6InllcyJ9"
####################
#### DECRYPTION ####
####################
verifier = Paseto::V4::Public.new('public_key')
# when initialized with a public key, only verification/decoding can be performed
# if encode is called, error is raised
verifier.encode({'foo' => 'bar'})
# => ArgumentError
signed_token = "v4.public.eyJleHAiOiIyMDIzLTA1LTEwVDExOjQ2OjA0KzA5OjAwIiwiaWF0IjoiMjAyMy0wNS0xMFQxMDo0NjowNCswOTowMCIsIm5iZiI6IjIwMjMtMDUtMTBUMTA6NDY6MDQrMDk6MDAiLCJjb21wYW55IjoibW9uc3RhcmxhYiJ9taKQPCARAZHv85xk7yaWPDeWHaHt981eHmoiYIrIcA-monnIbMax2EDxIjObgr6qLLuYzAH4BK5N6q0TJANeBg.eyJ2aWV3YWJsZSI6InllcyJ9"
verifier.decode(signed_token)
# => <Paseto::Result
# claims={
# "exp"=>"2023-05-10T11:18:41+09:00",
# "iat"=>"2023-05-10T10:18:41+09:00",
# "nbf"=>"2023-05-10T10:18:41+09:00",
# "company"=>"monstarlab"},
# footer={"viewable"=>"yes"}
# >
Conclusion
In conclusion, we have explored the vulnerabilities that can arise from the careless use of JSON Web Tokens (JWTs). While JWTs can serve as an effective means of incorporating authentication into a system when used correctly, their flexible specification can potentially lead to implementation errors and subsequent security issues.
To address these concerns, PASETO has been introduced as an alternative solution with specific design goals in mind:
- Simplicity of use: PASETO simplifies the token creation process by requiring only the specification of the
purpose
andversion
parameters. - Resistance to implementation errors: Unlike JWTs, PASETO eliminates the need to choose from a wide range of potentially insecure cryptographic algorithms, thereby reducing the risk of implementation mistakes.
PASETO takes a developer-first approach to security tokens by streamlining the decision-making process for developers. By offering two distinct purposes, namely the choice between a symmetric or asymmetric security model, PASETO automatically selects the most suitable options for authenticated encryption and digital signatures. This ensures that your tokens remain secure and immune to cryptographic vulnerabilities.
Overall, PASETO provides a more robust and straightforward approach to security token management, mitigating the risks associated with JWTs while maintaining a high level of security for your system.
References
- PASETO: The JWT Killer? - article by Sandesh Dahake
- PASETO: Platform-Agnostic Security Tokens - Github repository
- Why PASETO is better than JWT for token-based authentication? - Youtube video by Tech School
- Introducing JPaseto: Security Tokens For Java - article by Brian Demers
- Encode or Decode PASETO
- Paseto - Github repository
Article Photo by Erik Mclean