webgates-codecs 1.0.0

Framework-agnostic JWT codecs and validation helpers for webgates.
Documentation

webgates-codecs

User-focused JWT codecs and validation helpers for the webgates ecosystem.

webgates-codecs is the codec layer of the workspace. It gives you the pieces you need to encode, decode, and validate JWT payloads without pulling in HTTP, cookies, middleware, or framework-specific integration.

If webgates-core is the domain model and webgates is the higher-level auth stack, webgates-codecs is the crate you reach for when you specifically need token codecs and JWT validation building blocks.

Who this crate is for

Use webgates-codecs when you want to:

  • encode and decode JWTs in framework-agnostic Rust code
  • work directly with JwtClaims and RegisteredClaims
  • validate tokens against an expected issuer
  • manage ES384 signing and verification keys
  • publish or consume JWKS-compatible public key material
  • build custom integrations that need token handling without pulling in transport layers

If you want higher-level authentication services or gates, use webgates. If you want domain types only, use webgates-core. If you want transport integration, use webgates-axum or webgates-tonic.

What you work with in this crate

Most developers can approach this crate through five concepts:

  • Codec is the abstraction for encoding and decoding payloads
  • JsonWebToken<T> is the JWT implementation of that abstraction
  • RegisteredClaims and JwtClaims<T> model token contents
  • JwtValidationService validates raw token strings against application expectations
  • jwt::jwks supports public-key distribution and distributed verification

Install

[dependencies]
webgates-codecs = "1.0.0"
webgates-core = "1.0.0"

Minimum supported Rust version: 1.91.

The mental model

The easiest way to understand this crate is:

  1. your application data lives in some payload type
  2. Codec defines how payloads are encoded and decoded
  3. JsonWebToken<T> is the JWT implementation of that codec
  4. JwtValidationService adds application-level validation such as expected issuer checks
  5. jwt::jwks helps model public verification keys for distributed systems

Quick start

use std::sync::Arc;

use webgates_codecs::jwt::{
    JsonWebToken,
    JsonWebTokenOptions,
    JwtClaims,
    JwtValidationResult,
    JwtValidationService,
    RegisteredClaims,
};
use webgates_codecs::Codec;
use webgates_core::accounts::Account;
use webgates_core::groups::Group;
use webgates_core::permissions::Permissions;
use webgates_core::roles::Role;
use uuid::Uuid;

type AppClaims = JwtClaims<Account<Role, Group>>;

let codec = Arc::new(JsonWebToken::<AppClaims>::new_with_options(
    JsonWebTokenOptions::default(),
));

let claims = JwtClaims::new(
    Account {
        account_id: Uuid::now_v7(),
        user_id: "user@example.com".to_string(),
        roles: vec![Role::User],
        groups: vec![Group::new("engineering")],
        permissions: Permissions::new(),
    },
    RegisteredClaims::new("my-app", 4_102_444_800),
);

let encoded = codec.encode(&claims)?;
let decoded = codec.decode(&encoded)?;

assert!(decoded.has_issuer("my-app"));

let validation_service = JwtValidationService::new(Arc::clone(&codec), "my-app");

match validation_service.validate_token(std::str::from_utf8(&encoded)?) {
    JwtValidationResult::Valid(valid_claims) => {
        assert_eq!(valid_claims.custom_claims.user_id, "user@example.com");
    }
    JwtValidationResult::InvalidToken => {
        panic!("expected a valid token");
    }
    JwtValidationResult::InvalidIssuer { expected, actual } => {
        panic!("unexpected issuer mismatch: expected {expected}, got {actual}");
    }
}
# Ok::<(), Box<dyn std::error::Error>>(())

Core concepts

1. Codec is the abstraction

The Codec trait gives you a stable way to encode and decode typed payloads.

The important methods are:

  • Codec::encode
  • Codec::decode

This keeps token handling behind a simple abstraction that can be reused by higher-level crates.

2. JsonWebToken<T> is the JWT implementation

JsonWebToken<T> is the main codec you will use in this crate.

Use it when you want to:

  • sign JWTs
  • verify JWTs
  • work with strongly typed claims
  • keep JWT behavior behind the Codec trait

3. RegisteredClaims and JwtClaims<T> model token contents

RegisteredClaims stores the standard JWT fields such as:

  • issuer
  • subject
  • audience
  • expiration time
  • issued-at time
  • token id
  • optional session id

JwtClaims<T> combines those standard claims with your application-specific payload.

This makes it easy to carry typed account data or other application state inside the token.

4. JwtValidationService validates at the boundary

JwtValidationService<C> is useful when you receive a raw token string and want a clear, typed validation step.

It performs:

  1. decode through the configured codec
  2. issuer validation against the expected issuer

Important note: lower-level JWT checks such as signature verification, algorithm handling, and expiration checks remain owned by the configured codec.

5. jwt::jwks supports distributed verification

If your system separates token issuance and token verification, the JWKS helpers let you model public keys in a standard format.

Key types include:

  • EcP384Jwk
  • JwksDocument
  • JwksProvider

These are especially useful for auth authorities and resource servers that need shared public verification material.

Production guidance

Use stable ES384 keys

If you want a node to bootstrap its local key files on startup, you can ask webgates-codecs to create them the first time the process runs and then reuse them on later starts:

use webgates_codecs::jwt::{Es384KeyPairLoader, JwtClaims};
use webgates_core::accounts::Account;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

# async fn demo() -> Result<(), Box<dyn std::error::Error>> {
let key_pair = Es384KeyPairLoader::new(
    "./var/keys/jwt-es384-private.pem",
    "./var/keys/jwt-es384-public.pem",
)
.initialize_if_required()
.await?;

let jwt_codec = key_pair.to_codec::<JwtClaims<Account<Role, Group>>>()?;
# let _ = jwt_codec;
# Ok(())
# }

This loader returns an error if only one of the two files already exists, because that usually indicates a broken or partial deployment state. It also hides the raw file reads so startup code can focus on wiring rather than filesystem details.

If your node also publishes JWKS, you can build an authority from the same loaded key pair:

use webgates_codecs::jwt::authority::JwtAuthority;
use webgates_codecs::jwt::{Es384KeyPairLoader, JwtClaims};

# async fn demo() -> Result<(), Box<dyn std::error::Error>> {
let key_pair = Es384KeyPairLoader::new(
    "./var/keys/jwt-es384-private.pem",
    "./var/keys/jwt-es384-public.pem",
)
.initialize_if_required()
.await?;

let authority = key_pair.to_authority::<JwtClaims<()>>()?;
# let _ = authority;
# Ok(())
# }

JsonWebTokenOptions::default() uses an embedded ES384 development keypair. That is convenient for tests and local development, but it is not suitable when tokens must survive restarts or be validated across multiple instances.

For production, provide explicit ES384 key material:

use webgates_codecs::jwt::{JsonWebToken, JsonWebTokenOptions, JwtClaims};
use webgates_core::accounts::Account;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

type AppClaims = JwtClaims<Account<Role, Group>>;

let private_pem = std::fs::read("/run/secrets/jwt-es384-private.pem")?;
let public_pem = std::fs::read("/run/secrets/jwt-es384-public.pem")?;

let codec = JsonWebToken::<AppClaims>::new_with_options(
    JsonWebTokenOptions::from_es384_pem(&private_pem, &public_pem)?,
);
# let _ = codec;
# Ok::<(), Box<dyn std::error::Error>>(())

Verification-only nodes

If a node only validates tokens and never signs them, use verification-only options:

use webgates_codecs::jwt::{JsonWebToken, JsonWebTokenOptions, JwtClaims};
use webgates_core::accounts::Account;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

type AppClaims = JwtClaims<Account<Role, Group>>;

let public_pem = std::fs::read("/run/secrets/jwt-es384-public.pem")?;

let codec = JsonWebToken::<AppClaims>::new_with_options(
    JsonWebTokenOptions::for_es384_verification_only(&public_pem)?,
);
# let _ = codec;
# Ok::<(), Box<dyn std::error::Error>>(())

JWKS-backed verification

For JWKS-backed verification with strict kid selection:

use webgates_codecs::jwt::jwks::EcP384Jwk;
use webgates_codecs::jwt::{JsonWebToken, JsonWebTokenOptions, JwtClaims};
use webgates_core::accounts::Account;
use webgates_core::groups::Group;
use webgates_core::roles::Role;

type AppClaims = JwtClaims<Account<Role, Group>>;

let jwk = EcP384Jwk::from_public_key_pem("auth-key-1", public_pem.as_bytes())?;
let codec = JsonWebToken::<AppClaims>::new_with_options(
    JsonWebTokenOptions::for_es384_jwks_keys(&[jwk])?,
);
# let _ = codec;
# Ok::<(), Box<dyn std::error::Error>>(())

Error model

This crate exposes:

  • Error
  • CodecsError
  • JwtError
  • CodecOperation
  • JwtOperation

Use these when you want structured handling of codec failures and JWT processing failures.

Which crate should you use?

  • use webgates-core when you only want domain types and authorization primitives
  • use webgates-codecs when you specifically need JWT codecs and validation helpers
  • use webgates when you want the higher-level auth stack
  • use webgates-axum when you want Axum transport integration
  • use webgates-tonic when you want tonic server-side transport integration

Recommended onboarding path

If you are new to this crate, I recommend this order:

  1. Codec
  2. jwt::RegisteredClaims
  3. jwt::JwtClaims<T>
  4. jwt::JsonWebToken<T>
  5. jwt::validation_service::JwtValidationService
  6. jwt::jwks

Related crates

  • webgates-core - shared account, role, group, permission, and error primitives
  • webgates - higher-level authentication and authorization services
  • webgates-axum - Axum integration layer for routing and request handling

Validation

Before merging changes in this crate, run:

cargo fmt --all --check
cargo clippy --workspace --all-targets --deny warnings
cargo test -p webgates-codecs --all-targets