#jwt #openid-connect #jose #key-id

bbjwt

A simple to use, well documented JWT validation library, mainly for validating OpenID Connect ID Tokens

5 unstable releases

0.4.0 Jan 30, 2024
0.3.0 Nov 7, 2023
0.2.2 Jun 6, 2023
0.2.1 Jan 13, 2023
0.2.0 Jan 12, 2023

#315 in Cryptography

Download history 8/week @ 2023-12-31 28/week @ 2024-01-28 22/week @ 2024-02-11 56/week @ 2024-02-18 54/week @ 2024-02-25 116/week @ 2024-03-03 134/week @ 2024-03-10 95/week @ 2024-03-17 66/week @ 2024-03-24 103/week @ 2024-03-31 55/week @ 2024-04-07 64/week @ 2024-04-14

289 downloads per month

MIT license

73KB
1K SLoC

JWT validation library for basebox (and maybe others :-) )

Build Status crates.io docs.rs

Synopsis

This lib was created to provide a straight forward, simple and reliable way to validate JWTs against a set of public keys loaded from a URL. We at basebox use it to validate OpenID Connect ID Tokens (which are JWTs) using the set of public keys published by the OpenID server (e.g. Keycloak).

It provides the following features:

  • Download a set of public keys from a URL (a JSON Web Key Set)
  • Provide an entry point to update the keyset if necessary
  • Parse JWTs and validate them using the key(s) in the downloaded keyset.

And that's it.

Besides, we designed bbjwt to meet the following requirements:

  • No unsecure code
  • Never panic
  • No lifetime specifiers in the API
  • Asynchronous
  • Thread safe

Algorithm Support

The following table shows all signing algorithms supported by bbjwt, along with some info about their usage in JWKs, JWTs etc.

Note Please note that support for Ed448 and ES512 signatures was dropped in bbjwt 0.3.0.*

Name JOSE "kty" JOSE "alg" JOSE "curve"
RSA256 RSA RS256
RSA384 RSA RS384
RSA512 RSA RS512
ES256 EC ES256 P-256
ES384 EC ES384 P-384
Ed25519 OKP EdDSA Ed25519

Encrypted JWTs are not supported.

BTW, if you have the choice, use Ed25519. It is safe and fast.

Why yet another Rust JWT validation lib?

We tried various other Rust JWT libraries, but none worked for us. Problems were complicated APIs, lacking documentation and/or functionality. This is our attempt at doing better :-)

Usage

To validate JWTs, you have to have the issuer's public keys available. Using bbjwt, you can get them either by downloading them from a URL provided by the issuer, or you load them from a local buffer/file.

Download public keys from a URL

See the following example:

use bbjwt::KeyStore;

#[tokio::main]
async fn main() {

  // bbjwt provides a function to determine the public keyset URL by loading discovery
  // info from the issuer; this is common for OpenID Connect servers.

  // If you are using Keycloak, you can use this convenience function to get the discovery
  // endpoint URL; all you need is the base URL and the realm name:
  let discovery_url = KeyStore::keycloak_discovery_url(
    "https://server.tld", "testing"
  ).unwrap();

  // If you're not using Keycloak, the URL might be different.
  let discovery_url = "https://idp-host.tld/.well-known/discovery";

  // Call IdP's discovery endpoint to query the keyset URL; this is a common feature on
  // OpenID Connect servers.
  let keyset_url = KeyStore::idp_certs_url(discovery_url).await.unwrap();

  // Now we can load the keys into a new KeyStore:
  let keystore = KeyStore::new_from_url(&keyset_url).await.unwrap();
}

Using public keys from memory

When loading public keys from local file or buffer, you can either load a JWK JSON or a PEM encoded text. JWKs contain all required info to identify the type of key, but for PEM you need to use the function that corresponds to the type of key.

See the following example:

use bbjwt::{KeyStore, KeyAlgorithm, EcCurve};

#[tokio::main]
async fn main() {
  // Create an empty keystore
  let mut keystore = KeyStore::new().await.unwrap();

  // Load public key from a JWK JSON; see
  // https://openid.net/specs/draft-jones-json-web-key-03.html#ExampleJWK
  let json_key = r#"
  {
    "kty":"RSA",
    "use":"sig",
    ... abbreviated ...,
  }"#;
  // Add the key
  keystore.add_key(json_key).await;

  let pem_key = r#"-----BEGIN PUBLIC KEY-----
..."#;

  // Load a RSA key from a PEM buffer
  keystore.add_rsa_pem_key(
    pem_key,
    Some("key-rsa"),
    KeyAlgorithm::RS256
  ).await.unwrap();

  // Load a EC key from a PEM buffer
  keystore.add_ec_pem_key(
    pem_key,
    Some("key-ec"),
    EcCurve::P256,
    KeyAlgorithm::ES256
  ).await.unwrap();

  // Load EdDSA key from a PEM buffer
  keystore.add_ec_pem_key(
    pem_key,
    Some("key-ed"),
    EcCurve::Ed25519,
    KeyAlgorithm::EdDSA
  ).await.unwrap();

  // You can add more keys; in this case, the keys should have an ID and the JWT to be
  // validated should have a "kid" claim. Otherwise, bbjwt uses the first key in the set.
}

Validating JWTs

JWTs are passed as Base64 encoded strings; for details about this format, see e.g. https://jwt.io.

To validate a JWT, you pass the base64 encoded JWT and a vector of ValidationSteps into validate_jwt. bbjwt provides a convenience function named default_validations to create a vector of default validation steps.

If the JWT is valid, validate_jwt returns all claims that the JWT contains (header and payload).

Example:

use bbjwt::{KeyStore, default_validations, validate_jwt};

#[tokio::main]
async fn main() {
  // Create a keystore; see examples above
  let keystore = KeyStore::new_from_url("https://server.tld/keyset").await.unwrap();

  // Validate a JWT
  let jwt = validate_jwt(
    "<Base64 encoded JWT>",
    &default_validations(
      // required value for the "iss" claim
      "https://idp.domain.url/realm/testing",
      None,
      None),
    &keystore
  )
  .await
  .unwrap();

  // Read some claims (JWT fields)
  assert_eq!(jwt.claims["nonce"].as_str().unwrap(), "UZ1BSZFvy7jKkj1o9p3r7w");
}

Copyright (c) 2022 basebox GmbH, all rights reserved.

License: MIT

Made with ❤️ and Emacs :-)

Dependencies

~16–31MB
~580K SLoC