Authentication and Authorization
Volga provides a flexible set of tools for implementing authentication and authorization in your web applications. It supports both Basic Authentication and Bearer Token (JWT)-based authentication, with built-in facilities for access control based on roles, permissions, or custom logic.
Basic Authentication
Basic authentication is a simple mechanism for verifying user credentials (username and password) via the Authorization: Basic
HTTP header.
Dependencies
[dependencies]
volga = { version = "0.6.1", features = ["basic-auth"] }
Example
use volga::{
App, HttpResult,
headers::WWW_AUTHENTICATE,
auth::Basic,
status, ok
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut app = App::new();
app.map_get("/protected", protected);
app.run().await
}
async fn protected(auth: Basic) -> HttpResult {
let (expected_user, expected_pass) = get_credentials_from_db().await;
if auth.validate(&expected_user, &expected_pass) {
ok!("Access granted")
} else {
status!(401, "Unauthorized", [
(WWW_AUTHENTICATE, "Basic realm=\"Restricted area\"")
])
}
}
async fn get_credentials_from_db() -> (String, String) {
// In a real application, retrieve this securely
("foo".into(), "bar".into())
}
The Basic
struct parses the HTTP header and provides validate()
and validate_base64()
for credential comparison.
Bearer Token Authentication (JWT)
JWT (JSON Web Tokens) are more robust and support structured claims. This allows you to implement both authentication and fine-grained authorization.
Dependencies
[dependencies]
volga = { version = "0.6.1", features = ["jwt-auth-full"] }
Token Generation
use std::{ops::Add, time::{SystemTime, UNIX_EPOCH, Duration}};
use serde::{Serialize, Deserialize};
use volga::{
App, Json, HttpResult,
auth::{Claims, BearerTokenService, EncodingKey},
ok, status, bad_request
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET must be set");
let mut app = App::new()
.with_bearer_auth(|auth| {
auth.set_encoding_key(EncodingKey::from_secret(secret.as_bytes()))
});
app.map_post("/generate", generate);
app.run().await
}
async fn generate(payload: Json<Payload>, bts: BearerTokenService) -> HttpResult {
if payload.client_id != "foo" || payload.client_secret != "bar" {
return status!(401, "Invalid credentials");
}
let exp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.add(Duration::from_secs(300))
.as_secs();
let claims = Claims {
sub: "user@email.com".into(),
company: "Awesome Co.".into(),
role: "admin".into(),
exp,
};
let token = bts.encode(&claims)?.to_string();
ok!(AuthData { access_token: token })
}
#[derive(Claims, Serialize, Deserialize)]
struct Claims {
sub: String,
company: String,
role: String,
exp: u64,
}
#[derive(Serialize)]
struct AuthData {
access_token: String,
}
#[derive(Deserialize)]
struct Payload {
client_id: String,
client_secret: String,
}
JWT Usage
The authorize() middleware provides tools to implement roles-based or permissions-based access control and can be difined for a single route, group of routes or the entire application.
use serde::Deserialize;
use volga::{
App, ok,
auth::{Claims, DecodingKey, roles},
};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let secret = std::env::var("JWT_SECRET")
.expect("JWT_SECRET must be set");
let mut app = App::new()
.with_bearer_auth(|auth| {
auth.set_decoding_key(DecodingKey::from_secret(secret.as_bytes()))
});
app.map_get("/me", me)
.authorize::<Claims>(roles(["admin", "user"]));
app.run().await
}
async fn me() -> &'static str {
"Hello from protected route"
}
#[derive(Claims, Deserialize)]
struct Claims {
sub: String,
company: String,
role: String,
exp: u64,
}
Warning
To ensure proper JWT functionality, you must use the same JWT_SECRET
both when generating the token and when validating it on protected routes. This secret is used to sign the token during generation and to verify the signature during validation. If these values differ - the token will be rejected as invalid.
Defining Claims
The jwt-auth-full
feature enables the Claims
derive macro for defining JWT claims. Alternatively, you can define claims using:
claims!
macro
use volga::auth::claims;
claims! {
#[derive(Deserialize)]
struct Claims {
sub: String,
role: String,
permissions: Vec<String>,
}
}
Manual Implementation
use volga::auth::AuthClaims;
#[derive(Deserialize)]
struct Claims {
sub: String,
role: String,
permissions: Vec<String>,
}
impl AuthClaims for Claims {
fn role(&self) -> Option<&str> {
Some(&self.role)
}
fn permissions(&self) -> Option<&[String]> {
Some(&self.permissions)
}
}
Declarative Access Control with Authorizer
Volga provides a powerful Authorizer
system that lets you define access rules declaratively.
Built-in Authorizers
role("admin")
: single-role check.roles(["admin", "user"])
: multi-role check.permission("write")
: single permission.permissions(["read", "write"])
: multiple permissions.predicate(|claims| ...)
: custom logic.
Example
use serde::Deserialize;
use volga::auth::{
AuthClaims, Authorizer
role, roles, permission, permissions, predicate
};
#[derive(Deserialize)]
struct MyClaims {
role: String,
permissions: Vec<String>,
}
impl AuthClaims for MyClaims {
fn role(&self) -> Option<&str> {
Some(&self.role)
}
fn permissions(&self) -> Option<&[String]> {
Some(&self.permissions)
}
}
fn main() {
let admin = role("admin");
let editors = roles(["editor", "contributor"]);
let can_write = permission("write");
let access_policy = admin.or(editors).and(can_write);
let user = MyClaims {
role: "editor".into(),
permissions: vec!["write".into()],
};
assert!(access_policy.validate(&user));
}