#session #tokens #session-token

signed_tokens

Creates and verifies digitally-signed tokens suitable for authenticated session IDs

1 unstable release

0.2.0 Jul 10, 2024

#5 in #session-token

MIT license

21KB
248 lines

Signed Tokens

CI

A simple Rust crate for creating and verifying HMAC-signed tokens, with multiple rotating keys.

The canonical use-case for this is authenticated session tokens. After a user successfully signs in, your system should:

  1. Generate a random session ID, perhaps using the uuid crate
  2. Put the account information into a cache (e.g., Redis) using the session ID as the key
  3. Use this crate to digitally sign the session ID and encode it in base64
  4. Include the signed token as a secure, HTTP-only cookie

During subsequent requests, use this crate to verify the signed token and retrieve the session ID so you can look up the session data in your cache.

Usage

After the user successfully signs in, generate a random value for the session ID--a UUID works well for this, but you can use any unique value:

use uuid::Uuid;

let session_id = Uuid::new_v4().to_string();

To digitally sign the session ID, you'll also need some secret keys. Your server is the only one that needs to know about these keys, but they should remain the same after a server restart so that existing sessions will still be valid. You can read them from environment variables, or a file, or a secrets manager service, or whatever. But they should remain secret to your server and never be added directly to your source code.

use signed_tokens::SigningKey;

let signing_keys = vec![
    SigningKey::new(env::var("SESSION_SIGNING_KEY_1").unwrap()),
    SigningKey::new(env::var("SESSION_SIGNING_KEY_2").unwrap()),
    SigningKey::new(env::var("SESSION_SIGNING_KEY_3").unwrap()),
];

You can have up to 255 signing keys. Each key has a status, which defaults to SigningKeyStatus::SignAndVerify. This can be changed to VerifyOnly when you want to stop signing new tokens with the key, but allow existing tokens to be verified with it.

When you sign a session ID, this crate will randomly choose one of the sign-and-verify keys. The chosen key's slice index will be added to the signed and encoded token so that the library knows which key to use when later verifying the token.

To sign your session ID, pass it and your slice of signing keys to the sign() function:

let token = signed_tokens::sign(&session_id, &signing_keys)?;
let url_safe_base64_token = token.to_string();

The sign() method returns a SignedToken struct, which wraps a binary buffer containing the chosen signing key index, the payload (your session ID in this case) and an HMAC signature. This can be turned into a base64-encoded String using the to_string() method.

When you respond to the client, include the base64-encoded string as a secure HttpOnly cookie. When this cookie comes back in subsequent requests, verify it using the verify() function.

// Use the same set of signing keys as you did when signing
let verified_token = signed_tokens::verify(&token_from_request_cookie, &signing_keys)?;
let session_id = verified_token.payload();

// look up account info in your cache using `session_id`...

The VerifiedToken returned from verify() contains not only the payload you passed when signing the token, but also the SigningKeyStatus of the key used to verify the token. This is useful for rotating keys over time (see next section for more details).

The verify() function will return an error Result if any of the following occur:

  • The token has been tampered with--i.e., the payload or signature has been changed since signing
  • The token contains a signing key index that is no longer in the provided array of signing keys
  • The token is not a valid base64 string
  • The token is too short to be a valid token

Rotating Keys Over Time

It's a good idea to rotate your signing keys over time, even if they are never compromised. To do so without interruption to your clients, follow this set of steps:

  1. Change the status of the key you want to replace to VerifyOnly. This will deprecate the key so that existing tokens will still verify, but no new tokens will be signed using that key.
  2. After a successful verification, if the VerifiedToken.key_status() method returns SigningKeyStatus::VerifyOnly, call the sign() method again passing the payload from the VerifiedToken to generate a new SignedToken using an active signing key. You can then include this new signed token in your response as the new value for your session cookie. The client will then send this new token back to your server during subsequent requests.
  3. After enough time has passed, replace the deprecated SigningKey with a new active one. If a client previously got a token signed with the deprecated key and never returned to your site after it was deprecated, that client's token will no longer verify. The client can simply sign in again to get a refreshed session token.

Dependencies

~1–1.5MB
~33K SLoC