salvo-auth
Salvo Authentication
JWT Authentication
[dependencies]
salvo = { version = "0.89.3", features = ["jwt-auth"] }
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }
chrono = "0.4"
JwtAuth::new takes a decoder (not a raw secret string). Use ConstDecoder::from_secret for HMAC or ConstDecoder::from_rsa_pem / from_ec_pem for asymmetric keys.
use salvo::prelude::*;
use salvo::jwt_auth::{ConstDecoder, HeaderFinder, JwtAuth, JwtAuthDepotExt, QueryFinder};
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::{Deserialize, Serialize};
const SECRET_KEY: &str = "your-secret-key-at-least-32-bytes";
#[derive(Debug, Serialize, Deserialize)]
struct JwtClaims {
sub: String,
exp: i64,
role: String,
}
#[derive(Deserialize)]
struct LoginRequest { username: String, password: String }
#[derive(Serialize)]
struct LoginResponse { token: String }
#[handler]
async fn login(body: JsonBody<LoginRequest>) -> Result<Json<LoginResponse>, StatusError> {
let req = body.into_inner();
if req.username != "admin" || req.password != "password" {
return Err(StatusError::unauthorized());
}
let claims = JwtClaims {
sub: req.username,
exp: (chrono::Utc::now() + chrono::Duration::hours(24)).timestamp(),
role: "user".to_string(),
};
let token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SECRET_KEY.as_bytes()),
)
.map_err(|_| StatusError::internal_server_error())?;
Ok(Json(LoginResponse { token }))
}
#[handler]
async fn protected(depot: &mut Depot) -> Result<String, StatusError> {
let data = depot.jwt_auth_data::<JwtClaims>()
.ok_or_else(StatusError::unauthorized)?;
Ok(format!("Hello, {}! Role: {}", data.claims.sub, data.claims.role))
}
#[tokio::main]
async fn main() {
let auth: JwtAuth<JwtClaims, _> = JwtAuth::new(ConstDecoder::from_secret(SECRET_KEY.as_bytes()))
.finders(vec![
Box::new(HeaderFinder::new()),
Box::new(QueryFinder::new("token")),
]);
let router = Router::new()
.push(Router::with_path("login").post(login))
.push(Router::with_path("protected").hoop(auth).get(protected));
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
Available finders: HeaderFinder, QueryFinder, FormFinder, CookieFinder. Available decoders include ConstDecoder and (with oidc feature) OidcDecoder.
Depot extension
JwtAuthDepotExt provides:
depot.jwt_auth_token() -> Option<&str>depot.jwt_auth_data::<C>() -> Option<&TokenData<C>>depot.jwt_auth_state() -> JwtAuthState(Authorized/Unauthorized/Forbidden)
Basic Authentication
[dependencies]
salvo = { version = "0.89.3", features = ["basic-auth"] }
use salvo::prelude::*;
use salvo::basic_auth::{BasicAuth, BasicAuthValidator};
struct MyValidator;
impl BasicAuthValidator for MyValidator {
async fn validate(&self, username: &str, password: &str, depot: &mut Depot) -> bool {
if username == "admin" && password == "password" {
depot.insert("user_role", "admin");
true
} else {
false
}
}
}
#[tokio::main]
async fn main() {
let auth = BasicAuth::new(MyValidator);
let router = Router::with_path("admin").hoop(auth).get(admin_handler);
let acceptor = TcpListener::new("0.0.0.0:8080").bind().await;
Server::new(acceptor).serve(router).await;
}
BasicAuth::new(v).set_realm("myapp") customizes the realm. Retrieve the authenticated user via depot.basic_auth_username() (BasicAuthDepotExt).
Custom Middleware (Bearer / API Key)
use salvo::prelude::*;
#[handler]
async fn auth_middleware(req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
let token = req.header::<String>("Authorization")
.and_then(|h| h.strip_prefix("Bearer ").map(String::from));
match token.and_then(|t| validate_token(&t).ok()) {
Some(user_id) => {
depot.insert("user_id", user_id);
}
None => {
res.status_code(StatusCode::UNAUTHORIZED);
res.render(Json(serde_json::json!({"error": "Unauthorized"})));
ctrl.skip_rest();
}
}
}
fn validate_token(token: &str) -> Result<i64, ()> {
if token == "valid_token" { Ok(123) } else { Err(()) }
}
For an API key scheme, read req.header::<String>("X-API-Key") and follow the same pattern.
Role-Based Access Control
#[derive(Clone, PartialEq)]
enum Role { Admin, User, Guest }
fn require_role(required: Role) -> impl Handler {
#[handler]
async fn check(depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
// Pull required role from a closure-captured value in real code.
}
check
}
In practice, store the role in Depot during authentication, then build a small #[handler] that reads it and calls ctrl.skip_rest() with 403 on mismatch. Chain it after the auth middleware:
Router::with_path("admin")
.hoop(auth)
.hoop(require_admin)
.get(admin_handler);
Refresh Tokens
Issue a short-lived access token (e.g. 15 min) and a long-lived refresh token stored server-side (DB/Redis). On /refresh, validate the refresh token, look up the user, mint a new access token, and optionally rotate the refresh token.
#[handler]
async fn refresh(body: JsonBody<RefreshRequest>) -> Result<Json<TokenResponse>, StatusError> {
let user_id = validate_refresh_token(&body.into_inner().refresh_token)
.map_err(|_| StatusError::unauthorized())?;
let claims = JwtClaims {
sub: user_id.to_string(),
exp: (chrono::Utc::now() + chrono::Duration::minutes(15)).timestamp(),
role: "user".into(),
};
let access_token = encode(
&Header::default(),
&claims,
&EncodingKey::from_secret(SECRET_KEY.as_bytes()),
).map_err(|_| StatusError::internal_server_error())?;
Ok(Json(TokenResponse {
access_token,
refresh_token: generate_refresh_token(),
expires_in: 900,
}))
}
Salvo-specific Notes
JwtAuth::newrequires aJwtAuthDecoderimpl, not a&str. Passing a string will not compile.JwtAuthhas a type parameter for the claims type; annotate it (JwtAuth<JwtClaims, _>) when the compiler cannot infer it..force_passed(true)lets the request continue even when unauthorized so downstream handlers can decide; default isfalse.BasicAuthValidator::validateuses nativeasync fnin traits (noasync_traitcrate required).
Related Skills
- salvo-session: Session-based authentication
- salvo-cors: Configure CORS for authenticated APIs
- salvo-rate-limiter: Rate limit authentication endpoints
More from salvo-rs/salvo-skills
salvo-cors
Configure Cross-Origin Resource Sharing (CORS) and security headers. Use for APIs accessed from browsers on different domains.
15salvo-proxy
Implement reverse proxy to forward requests to backend services. Use for load balancing, API gateways, and microservices routing.
15salvo-timeout
Configure request timeouts to prevent slow requests from blocking resources. Use for protecting APIs from long-running operations.
15salvo-openapi
Generate OpenAPI documentation automatically from Salvo handlers. Use for API documentation, Swagger UI, and API client generation.
15salvo-path-syntax
Path parameter syntax guide for Salvo routing. Explains the `{}` syntax (v0.76+) vs deprecated `<>` syntax, with migration examples.
14salvo-tls-acme
Configure TLS/HTTPS with automatic certificate management via ACME (Let's Encrypt). Use for production deployments with secure connections.
14