gaseto
PASETO tokens for Gleam.
New to PASETO? See paseto.io, the article No Way, JOSE! on why it exists, and the specification.
A local token is encrypted with a shared key. A public token is signed
with a secret key and checked with the matching public key. Anyone can
read the claims in a public token.
Versions 3 and 4 are supported. Use version 4 unless you are limited to NIST-approved algorithms, then use version 3, which uses them throughout. Versions 1 and 2 are deprecated by the spec and not supported.
let assert Ok(key) = gaseto.local_key_from_hex(gaseto.PasetoV4, key_hex)
let claims = gaseto.new_claims() |> gaseto.with_subject("user-42")
let assert Ok(token) = gaseto.encrypt(claims, key)
let assert Ok(opened) = gaseto.decrypt(token, key, gaseto.new_rules())
gaseto.subject(opened)
// -> Some("user-42")
To send tokens in HTTP responses and check them on requests, use gaseto_http. To choose, load and make keys, see Keys.
Types
What to put in a new token. Claims are the facts a token carries, such as
who it is about and when it expires. They are stored as a JSON object in
the token. The spec reserves seven registered claims
(Registered Claims),
which mean the same as in JWT
(RFC 7519),
except that times are written as RFC 3339 text. You can add your own. A token can also have a footer and an implicit
assertion. Start with new_claims.
pub opaque type Claims
Limits that decode_footer checks before it parses a JSON footer. A
footer can be read before the token is checked, so it can come from
anyone, and a very long, deeply nested or key-heavy JSON footer can
overload a JSON parser. The spec requires libraries to offer such a check
(Storing JSON in the Footer).
pub type FooterRules {
FooterRules(max_length: Int, max_depth: Int, max_keys: Int)
}
Constructors
-
FooterRules(max_length: Int, max_depth: Int, max_keys: Int)Arguments
- max_length
-
The most bytes the footer may have.
- max_depth
-
How deeply objects and arrays may nest. 1 is a value with no object or array, and 2 is one object or array with no nesting inside.
- max_keys
-
The most keys the footer may have, counted over all objects.
A shared key for local tokens. Make one with local_key_from_hex.
pub opaque type LocalKey
What kind of token it is. The spec gives examples of what each is for, and Keys explains which key each needs.
pub type PasetoPurpose {
Local
Public
}
Constructors
-
LocalEncrypted with one shared key, so only someone with the key can read the claims or make tokens. Use it when the same app makes and reads the tokens.
-
PublicSigned with a secret key and checked with its public key. Anyone can read the claims, but only the holder of the secret key can make tokens. Use it when other services need to check tokens they must not make.
Which PASETO version a key and its tokens use. Each version is a fixed set of algorithms (Protocol Versions).
pub type PasetoVersion {
PasetoV3
PasetoV4
}
Constructors
-
PasetoV3AES-256-CTR with HMAC-SHA-384, and ECDSA with P-384. Use it when you are limited to NIST-approved algorithms.
-
PasetoV4XChaCha20 with BLAKE2b, and Ed25519. Use this one unless you need version 3.
A public key for verifying public tokens. Make one with public_key_from_hex.
pub opaque type PublicKey
What decrypt and verify check in a token: its times, the claim values
you require, and the implicit assertion. Start with new_rules.
pub opaque type Rules
A secret key for signing public tokens. Make one with secret_key_from_hex.
pub opaque type SecretKey
Values
pub fn allow_clock_skew(
rules: Rules,
up_to skew: duration.Duration,
) -> Rules
Accepts tokens whose times are off by up to this much, for when the clocks
of the servers that make and check tokens differ a little. It applies to
exp, nbf and iat. A negative duration counts as none. By default no
difference is allowed.
gaseto.new_rules() |> gaseto.allow_clock_skew(duration.seconds(30))
pub fn allow_no_expiration(rules: Rules) -> Rules
Accepts tokens without an exp (expiration) claim. If a token has one, it is still
checked.
pub fn allow_token_length(
rules: Rules,
up_to max_length: Int,
) -> Rules
Refuses tokens longer than this many bytes, before any cryptography. A token can come from anyone, and checking it takes time in proportion to its length, so a very long token could be used to waste CPU time. The default is 8192 bytes, Gaseto’s own choice, which is more than most tokens need and matches a common limit for HTTP headers.
gaseto.new_rules() |> gaseto.allow_token_length(up_to: 16_384)
pub fn audience(token: Token) -> option.Option(String)
The aud (audience) claim: who the token is meant for. See
with_audience.
pub fn checked_at(
rules: Rules,
time: timestamp.Timestamp,
) -> Rules
Checks exp, nbf and iat against this time instead of the current
time. Useful in tests.
pub fn decode_footer(
footer: BitArray,
rules: FooterRules,
decoder: decode.Decoder(a),
) -> Result(a, error.FooterDecodingError)
Decodes a JSON footer, after checking it against the rules. The check protects the JSON parser from footers that are very large, deeply nested, or have many keys. The spec requires libraries to offer such a check for JSON footers (Storing JSON in the Footer).
let key_identifier_decoder = decode.at(["kid"], decode.string)
gaseto.decode_footer(footer, gaseto.default_footer_rules, key_identifier_decoder)
pub fn decrypt(
token: String,
key: LocalKey,
rules: Rules,
) -> Result(Token, error.TokenValidationError)
Decrypts a local token and checks it against the rules. The key must
have the same version as the token. Tokens longer than 8192 bytes are
refused unless the rules allow more, see allow_token_length.
case gaseto.decrypt(token, key, gaseto.new_rules()) {
Ok(token) -> Ok(gaseto.subject(token))
Error(error.Expired(_)) -> Error("Please log in again")
Error(_) -> Error("Invalid token")
}
pub const default_footer_rules: FooterRules
At most 8192 bytes and 512 keys, with no nested objects or arrays. No nesting is the spec’s recommended default (Storing JSON in the Footer). The length and key limits are Gaseto’s own.
pub fn encrypt(
claims: Claims,
key: LocalKey,
) -> Result(String, error.TokenCreationError)
Creates an encrypted local token.
let assert Ok(token) = gaseto.encrypt(claims, key)
// token == "v4.local.…"
pub fn expect_implicit_assertion(
rules: Rules,
implicit_assertion: BitArray,
) -> Rules
Requires the token to have been made with this implicit assertion. An
implicit assertion is data that is covered by the tag or signature but not
stored in the token, so the reader has to know it already. See
with_implicit_assertion
(Implicit Assertions).
pub fn expires_at(
token: Token,
) -> option.Option(timestamp.Timestamp)
The exp (expiration) claim: when the token stops being valid. See
with_expires_at.
pub fn expires_in(
claims: Claims,
duration: duration.Duration,
) -> Claims
Sets exp (expiration) to this long after the token is created
(RFC 7519, exp).
After that, the token is refused (Validators).
pub fn footer(token: Token) -> BitArray
The footer, as raw bytes. It has been checked along with the token, unlike
unverified_footer. It is often JSON text: decode it with decode_footer,
or turn it into a String with bit_array.to_string. It is empty if the
token has none. See with_footer.
pub fn for_audience(rules: Rules, audience: String) -> Rules
Requires aud (audience) to be this value, so tokens meant for someone
else are refused. See with_audience.
pub fn for_subject(rules: Rules, subject: String) -> Rules
Requires sub (subject) to be this value, so only tokens about this
subject are accepted. See with_subject.
pub fn generate_local_key(version: PasetoVersion) -> LocalKey
Generates a new shared key. Generate a key once and keep it: a key
generated each time your app starts would change on every restart, which
makes all earlier tokens invalid. To store it, see local_key_to_hex.
let key = gaseto.generate_local_key(gaseto.PasetoV4)
pub fn generate_secret_key(version: PasetoVersion) -> SecretKey
Generates a new secret key. Get its public key with
secret_key_to_public_key. Generate a key once and keep it, as with
generate_local_key. To store it, see secret_key_to_hex.
let secret_key = gaseto.generate_secret_key(gaseto.PasetoV4)
let public_key = gaseto.secret_key_to_public_key(secret_key)
pub fn identified_by(
rules: Rules,
token_identifier: String,
) -> Rules
Requires jti (token identifier) to be this value. See
with_token_identifier.
pub fn issued_at(
token: Token,
) -> option.Option(timestamp.Timestamp)
The iat (issued at) claim: when the token was made. See
with_issued_at.
pub fn issued_by(rules: Rules, issuer: String) -> Rules
Requires iss (issuer) to be this value, so only tokens made by this
issuer are accepted. See with_issuer.
pub fn issuer(token: Token) -> option.Option(String)
The iss (issuer) claim: who made the token. See with_issuer.
pub fn local_key_from_hex(
version: PasetoVersion,
key: String,
) -> Result(LocalKey, error.KeyLoadingError)
Makes a shared key from 32 bytes written as hex.
let assert Ok(key) = gaseto.local_key_from_hex(gaseto.PasetoV4, key_hex)
pub fn local_key_to_hex(key: LocalKey) -> String
The key as hex, as local_key_from_hex reads it. For how to manage keys, see
Keys.
pub fn never_expires(claims: Claims) -> Claims
Leaves out exp (expiration), so the token never expires. Only use this
when you really need it. new_rules refuses such tokens unless the reader uses
allow_no_expiration.
pub fn new_claims() -> Claims
New claims for a token. By default the token expires one hour after it is
created, as the spec recommends
(Default Expiration Claims),
and iat is set to the time it is created.
let claims =
gaseto.new_claims()
|> gaseto.with_subject("user-42")
|> gaseto.expires_in(duration.minutes(15))
pub fn new_rules() -> Rules
Rules that require an exp claim, as the spec recommends
(Default Expiration Claims),
and check the times against the current time. A token is refused after its
exp (expiration), before its nbf (not before), or if its iat (issued
at) is in the future.
let rules =
gaseto.new_rules()
|> gaseto.issued_by("auth.example.com")
|> gaseto.for_audience("api")
pub fn not_before(
token: Token,
) -> option.Option(timestamp.Timestamp)
The nbf (not before) claim: when the token starts being valid. See
with_not_before.
pub fn payload(token: Token) -> String
The whole payload, a JSON object. Read your own claims from it with a decoder.
json.parse(gaseto.payload(token), decode.at(["role"], decode.string))
pub fn public_key_from_hex(
version: PasetoVersion,
key: String,
) -> Result(PublicKey, error.KeyLoadingError)
Makes a public key from hex. Version 3 takes a 49-byte compressed P-384 point. Version 4 takes a 32-byte Ed25519 public key.
let assert Ok(public_key) = gaseto.public_key_from_hex(gaseto.PasetoV4, public_key_hex)
pub fn public_key_from_pem(
version: PasetoVersion,
pem: String,
) -> Result(PublicKey, error.KeyLoadingError)
Makes a public key from a PEM file, as openssl pkey -pubout writes it.
let assert Ok(public_key) =
gaseto.public_key_from_pem(gaseto.PasetoV4, public_key_pem)
pub fn public_key_to_hex(key: PublicKey) -> String
The key as hex, as public_key_from_hex reads it.
pub fn secret_key_from_hex(
version: PasetoVersion,
key: String,
) -> Result(SecretKey, error.KeyLoadingError)
Makes a secret key from hex. Version 3 takes a 48-byte P-384 secret. Version 4 takes a 64-byte Ed25519 key: the seed followed by the public key.
let assert Ok(secret_key) = gaseto.secret_key_from_hex(gaseto.PasetoV4, secret_key_hex)
pub fn secret_key_from_pem(
version: PasetoVersion,
pem: String,
) -> Result(SecretKey, error.KeyLoadingError)
Makes a secret key from a PEM file: an Ed25519 key in PKCS #8 for version
4, or a P-384 key in SEC1 or PKCS #8 for version 3. openssl genpkey
writes PKCS #8, and openssl ecparam -genkey writes SEC1.
let assert Ok(secret_key) =
gaseto.secret_key_from_pem(gaseto.PasetoV4, secret_key_pem)
pub fn secret_key_to_hex(key: SecretKey) -> String
The key as hex, as secret_key_from_hex reads it. For how to manage keys, see
Keys.
pub fn secret_key_to_public_key(
secret_key: SecretKey,
) -> PublicKey
The public key that checks tokens signed with this secret key.
pub fn sign(
claims: Claims,
key: SecretKey,
) -> Result(String, error.TokenCreationError)
Creates a signed public token. Anyone can read its claims.
let assert Ok(token) = gaseto.sign(claims, secret_key)
// token == "v4.public.…"
pub fn subject(token: Token) -> option.Option(String)
The sub (subject) claim: who or what the token is about. See
with_subject.
pub fn token_identifier(token: Token) -> option.Option(String)
The jti (token identifier) claim: a unique identifier for the token.
See with_token_identifier.
pub fn unverified_footer(
token: String,
) -> Result(BitArray, error.TokenFormatError)
Reads the footer without checking the token. Use it to find which key to
use, for example from a kid (key identifier) field. Do not trust anything else in it until
the token has been decrypted or verified. It is empty if the token has no
footer.
let key_identifier_decoder = decode.at(["kid"], decode.string)
let assert Ok(footer) = gaseto.unverified_footer(token)
let assert Ok(key_identifier) =
gaseto.decode_footer(footer, gaseto.default_footer_rules, key_identifier_decoder)
pub fn verify(
token: String,
key: PublicKey,
rules: Rules,
) -> Result(Token, error.TokenValidationError)
Verifies a public token and checks it against the rules. The key must
have the same version as the token. Tokens longer than 8192 bytes are
refused unless the rules allow more, see allow_token_length.
let assert Ok(token) = gaseto.verify(token, public_key, rules)
gaseto.subject(token)
pub fn with_audience(claims: Claims, audience: String) -> Claims
Sets aud (audience): who the token is meant for, such as the service
that should accept it. Readers require it with for_audience, so a token
made for one service is refused by another
(RFC 7519, aud).
pub fn with_claim(
claims: Claims,
name: String,
value: json.Json,
) -> Claims
Adds a claim of your own. The registered names (iss, sub, aud,
exp, nbf, iat and jti) are refused when the token is created,
because the spec reserves them
(Registered Claims).
Use their own functions instead.
Adding a claim with the same name again replaces the first value.
gaseto.new_claims() |> gaseto.with_claim("role", json.string("admin"))
pub fn with_expires_at(
claims: Claims,
time: timestamp.Timestamp,
) -> Claims
Sets exp (expiration) to this time
(RFC 7519, exp).
After it, the token is refused (Validators).
Fractions of a second are left out, as the spec recommends
(Registered Claims).
pub fn with_footer(claims: Claims, footer: BitArray) -> Claims
Adds a footer: data stored in the token next to the claims. It is not
encrypted, so anyone can read it, even in a local token, but it cannot be
changed without breaking the token. A common use is a key identifier
(kid), so the reader knows which key to use
(Optional Footer,
Key-ID Support).
It can be any bytes, and is often JSON text. An empty footer means no
footer.
gaseto.new_claims() |> gaseto.with_footer(<<"{\"kid\":\"key-1\"}":utf8>>)
pub fn with_implicit_assertion(
claims: Claims,
implicit_assertion: BitArray,
) -> Claims
Adds an implicit assertion: data that is covered by the tag or signature
but not stored in the token. The reader must give the same value with
expect_implicit_assertion, or the token is refused. Use it to tie a token
to something both sides already know, such as a user identifier, without
putting it in the token
(Implicit Assertions).
pub fn with_issued_at(
claims: Claims,
time: timestamp.Timestamp,
) -> Claims
Sets iat (issued at), when the token was made, to this time instead of
the time the token is created
(RFC 7519, iat).
A token issued in the future is refused (Validators).
Fractions of a second are left out, as the spec recommends
(Registered Claims).
pub fn with_issuer(claims: Claims, issuer: String) -> Claims
Sets iss (issuer): who made the token, such as your login service.
Readers can require it with issued_by
(RFC 7519, iss).
pub fn with_not_before(
claims: Claims,
time: timestamp.Timestamp,
) -> Claims
Sets nbf (not before): the token is refused before this time. Use it for
a token that should only start working later
(RFC 7519, nbf).
Fractions of a second are left out, as the spec recommends
(Registered Claims).
pub fn with_subject(claims: Claims, subject: String) -> Claims
Sets sub (subject): who or what the token is about, usually a user
identifier. Readers can require it with for_subject
(RFC 7519, sub).
pub fn with_token_identifier(
claims: Claims,
token_identifier: String,
) -> Claims
Sets jti (token identifier): a unique identifier for this token. A
server can use it to remember tokens it has cancelled or already seen.
Readers can require it with identified_by
(RFC 7519, jti).