Authentication
Authentication answers one question: who is calling. The shape of
the answer is up to the Strategy — a JWT in the Authorization
header, an OAuth provider redirect, a basic-auth client_id:secret,
something app-specific. AuthGuard<S> runs the strategy on every
request its controller covers, returns a 401 when it fails, and
attaches the principal to the request so the handler reads it back with
Ctx<Principal>.
The framework ships JwtStrategy
(JSON Web Tokens), an
OAuth2 client, and
Argon2id password helpers.
Anything else is a Strategy you write yourself.
Install
Section titled “Install”cargo add nest-rs-authnBind the shipped JWT strategy
Section titled “Bind the shipped JWT strategy”The common case writes no strategy at all. AuthGuard<S> is generic
over the strategy, and the framework ships
JwtStrategy — a resource-server app
ships a single 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;Bind the guard on the controller, and every route under it requires a valid principal:
#[controller(path = "/users")]#[use_guards(AuthGuard, AuthzGuard)]pub struct UsersController { /* ... */ }Reading the principal back
Section titled “Reading the principal back”AuthGuard attaches the principal to the request before the handler
runs. The handler reads it with Ctx<P> — same primitive as any
other guard-attached context.
use nest_rs_http::Ctx;
async fn me(&self, auth: Ctx<Claims>) -> Json<User> { Json(self.svc.lookup(auth.sub).await?)}Ctx<Claims> is a typed lookup into the request’s extensions; missing
the guard means missing the type, and the framework returns 500 —
the access graph would already have failed the boot if the controller’s
import tree did not provide AuthGuard.
Advanced: write your own strategy
Section titled “Advanced: write your own strategy”When the credential is not a JWT — an API key, a session cookie,
something app-specific — a strategy implements one async method. It
returns the principal on success (Ok), or an AuthError describing
why it could not (Err). A strategy never issues a transport response
itself — a redirect-style flow (OAuth /authorize) is a plain handler,
so authentication stays a pure request → principal mapping.
use async_trait::async_trait;use nest_rs_authn::{AuthError, PrincipalIdentity, Strategy};use poem::Request;
#[injectable]pub struct ApiKeyStrategy { #[inject] svc: Arc<KeysService>,}
#[async_trait]impl Strategy for ApiKeyStrategy { type Principal = ApiKey;
async fn authenticate(&self, req: &mut Request) -> Result<ApiKey, AuthError> { let key = req.headers().get("x-api-key").and_then(|h| h.to_str().ok()); match key.and_then(|k| self.svc.lookup(k)) { Some(api_key) => Ok(api_key), None => Err(AuthError::MissingCredentials), } }}
// Every principal declares its audit identity — the value the framework// records as `actor_id` on the request span, so denials are attributable.impl PrincipalIdentity for ApiKey { fn actor_id(&self) -> Option<String> { Some(self.owner_id.to_string()) }}A custom strategy binds exactly like the shipped one:
AuthGuard<ApiKeyStrategy> behind a type alias, listed in providers,
bound with #[use_guards(...)].
One return type — principal or error
Section titled “One return type — principal or error”async fn authenticate(&self, req: &mut Request) -> Result<Self::Principal, AuthError>;A strategy maps a request to a principal and nothing else — it never issues a transport response. The two protocol styles both fit:
- Bearer protocols return
Ok(principal)on a valid token,Err(AuthError)on a missing or invalid one — the guard turns the error into a 401 withWWW-Authenticate: Bearer. - Redirect protocols (OAuth) keep the redirect out of the
strategy: the
/authorizeendpoint is a plain handler that returns a 302, the browser bounces to the provider and comes back to the callback, and only then does a strategy verify the resulting token.
The same trait serves both shapes because authentication is always a
pure request → principal mapping; the guard just runs authenticate
and reacts to the Result.
Public routes opt out
Section titled “Public routes opt out”A #[public] route still runs AuthGuard — but the guard does not
reject. If a credential is present and valid, the principal is
attached so a downstream policy guard can see who is calling. If
nothing is present, or the credential fails, the request continues
anonymously. Visitor-rule policy belongs in the authorization layer,
not in AuthGuard.
#[get("/health")]#[public]async fn health(&self) -> &'static str { "ok" }Credential extractors
Section titled “Credential extractors”nest-rs-authn exposes two header extractors so a Strategy does not
re-parse them:
bearer_token(&req) -> Option<&str>— pulls the token out ofAuthorization: Bearer <token>after trimming.basic_credentials(&req) -> Option<(String, String)>— decodesAuthorization: Basic <base64>and returns(client_id, client_secret), splitting on the first colon (RFC 7617).
Both return None rather than erroring on missing/malformed headers
— the strategy decides whether a missing credential is an AuthError
or simply an anonymous request the guard lets through on a #[public]
route.
Errors
Section titled “Errors”AuthError covers the failure modes a strategy hits:
MissingCredentials, InvalidToken, InvalidSignature,
InvalidAlgorithm, NotYetValid, Expired, Failed(String). Each
maps to a 401 body when returned by a Strategy; Failed collapses
to a generic "authentication failed" on the wire so the message
never leaks configuration detail.
A password-login service that wants to refuse without distinguishing
“wrong email” from “wrong password” returns CredentialError instead
— an opaque "invalid credentials" for any miss. See
Password for the timing-safe
shape of that path.
Pick the strategy that matches
Section titled “Pick the strategy that matches”- JWT — a resource server verifies a signed bearer token. HS256 (shared secret) or EdDSA (asymmetric, the resource server holds only the public key).
- OAuth2 — full authorization-code flow with PKCE, against any provider.
- Password — Argon2id hashing
plus
burn_verifyfor timing-safe rejection. - Issuer and resource server — the deployment pattern that lets a single token signer feed several verify-only APIs.
Once the principal is on the request, the authorization layer takes over — see Authorization.