JWT
JSON Web Tokens are the framework’s default bearer credential.
JwtStrategy<C> is generic over the claims type — your app picks the
shape, the framework verifies the signature, checks expiry, and hands
the claims to the handler. Signing and verifying both run through
JwtService, configured once via JwtConfig and injected wherever a
token is minted.
The implementation is a thin wrapper over
jsonwebtoken. The crate’s
Algorithm enum is re-exported as nest_rs_authn::Algorithm so an app
configures the algorithm without taking a direct dependency.
The three-line setup
Section titled “The three-line setup”A resource-server app declares a type alias and a tiny module:
pub type AppJwtStrategy = JwtStrategy<Claims>;pub type AuthGuard = nest_rs_authn::AuthGuard<AppJwtStrategy>;
#[module( imports = [nest_rs_authn::AuthnModule::for_root(None)], providers = [AppJwtStrategy, AuthGuard],)]pub struct AuthnModule;AuthnModule::for_root(None) reads JwtConfig from the environment;
pass a JwtConfig to pin it in code. The factory phase builds
JwtService once and registers it as global infrastructure —
controllers, login services, OAuth flows all inject the same instance.
One env var makes it work end to end — a shared HS256 secret (32 bytes minimum; anything shorter fails the boot):
export NESTRS_AUTHN__SECRET='a-32-byte-minimum-development-secret!!'With that set, JwtService signs and verifies against the same
secret: AuthGuard returns a 401 without a valid
Authorization: Bearer token and attaches the claims otherwise.
Every other knob is optional — the full env table and the
HS256-vs-EdDSA decision come later on this page.
What JwtStrategy does
Section titled “What JwtStrategy does”JwtStrategy<C> is an #[injectable] over an Arc<JwtService> and a
PhantomData<C>. On each request it:
- Pulls
Authorization: Bearer <token>out of the headers viabearer_token. - Calls
JwtService::verify::<C>(token)to verify the signature, checkexp/nbf, and deserialize the claims. - Returns
Ok(claims)on success.
A missing header yields AuthError::MissingCredentials; a bad
signature yields AuthError::InvalidSignature; an expired token
yields AuthError::Expired. AuthGuard maps every variant to a 401
with WWW-Authenticate: Bearer.
Claims — your shape, your business rules
Section titled “Claims — your shape, your business rules”The framework’s JwtStrategy<C> is generic. C is any
DeserializeOwned + Clone + Send + Sync + 'static type — usually a
struct with the standard sub, exp, plus whatever your app needs:
use serde::{Deserialize, Serialize};use uuid::Uuid;
#[derive(Clone, Debug, Serialize, Deserialize)]pub struct Claims { pub sub: Option<Uuid>, pub org_id: Uuid, pub roles: Vec<Role>, pub exp: u64,}The handler reads them back through Ctx<Claims>:
async fn create(&self, auth: Ctx<Claims>, body: Valid<Json<CreateUserDto>>) -> Result<Json<User>>{ Ok(Json( self.svc.create_in_org(body.into_inner(), auth.org_id).await?, ))}Signing a token
Section titled “Signing a token”A login or OAuth callback injects Arc<JwtService> and calls sign:
let claims = Claims { sub: Some(user.id), org_id: user.org_id, roles: user.roles.clone(), exp: self.jwt.expiry(),};let token = self.jwt.sign(&claims)?;sign is Result<String, AuthError> — on a verify-only service
(EdDSA with no private key), it returns AuthError::Failed(...).
expiry() returns now + ttl from JwtOptions::expires_in
(default one hour); ttl_secs() exposes the duration directly so the
issuer can return it alongside the token.
Verifying outside the guard
Section titled “Verifying outside the guard”Most code reaches the claims through Ctx<C>. A path that needs to
verify a token manually — say, the OAuth transaction cookie, or a
queue job carrying a signed envelope — injects Arc<JwtService> and
calls verify:
let claims: Claims = self.jwt.verify(token)?;The verification path runs Validation::leeway, checks nbf, and
matches aud only when JwtConfig::audience is set — otherwise the
audience check is disabled. Invalid signatures and expired tokens hit
tracing::warn! under nest_rs::authn.
Configuration
Section titled “Configuration”JwtConfig follows the framework-wide dual-path rule: every field is
settable from NESTRS_AUTHN__* env vars or from a pinned struct
passed to AuthnModule::for_root(config).
| Field | Env var | Notes |
|---|---|---|
secret | NESTRS_AUTHN__SECRET | HS256 shared secret |
private_key | NESTRS_AUTHN__PRIVATE_KEY | EdDSA PEM, issuer only |
public_key | NESTRS_AUTHN__PUBLIC_KEY | EdDSA PEM, verifier needs it |
leeway_secs | NESTRS_AUTHN__LEEWAY_SECS | Clock skew, default 30 |
audience | NESTRS_AUTHN__AUDIENCE | aud claim — unset disables the check |
JwtConfig::into_options walks the three key fields and chooses a
mode:
secretset → HMAC HS256 (both sign and verify).private_key+public_key→ EdDSA (sign and verify).public_keyalone → EdDSA verify-only — a resource server with no private key cannot mint a token even if it wanted to.- nothing → boot fails with
"no JWT key configured".
A misconfiguration (private_key without public_key) fails the boot
loudly, naming the offending env var.
HS256 vs EdDSA — pick on deployment shape
Section titled “HS256 vs EdDSA — pick on deployment shape”HS256 is fine when one binary both issues and verifies tokens.
Symmetric — anyone with the secret can mint a token. Convenient for
local development and single-process apps. Set
NESTRS_AUTHN__SECRET.
EdDSA is the production posture for split deployments. The issuer
holds the private PEM; every resource server holds the public PEM
only. Even a compromised resource server cannot mint a token. The
issuer sets both PRIVATE_KEY and PUBLIC_KEY; verifiers set only
PUBLIC_KEY.
See Issuer and resource server for the deployment pattern.
# generate an EdDSA key pairopenssl genpkey -algorithm ed25519 -out jwt.pemopenssl pkey -in jwt.pem -pubout -out jwt.pubGoing further
Section titled “Going further”- Issuer and resource server — the split-deploy pattern that pairs with EdDSA.
- OAuth2 — the JWT issuer is often the OAuth callback.
- Authorization — once the claims land on
the request, the authz layer builds an
Abilityfrom them.