Integrating with Rust Actix-web API
This guide explains how to implement JWT authentication in your Rust Actix-web API using AuthAction’s JWKS (JSON Web Key Set) endpoint. Public keys are cached in-process using a tokio::sync::RwLock, with automatic single-retry on key rotation.
Example Repository: For a complete working example, check out our example repository.
Prerequisites
Section titled “Prerequisites”Before you begin, ensure you have:
- Rust 1.75+: Install via rustup
- AuthAction Account: You’ll need your AuthAction tenant domain and API identifier
Configuration
Section titled “Configuration”1. Add Required Dependencies
Section titled “1. Add Required Dependencies”Add to your Cargo.toml:
[dependencies]actix-web = "4"jsonwebtoken = "9"reqwest = { version = "0.12", features = ["json"] }serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }dotenvy = "0.15"2. Configure AuthAction Settings
Section titled “2. Configure AuthAction Settings”Create a .env file in your project root:
AUTHACTION_DOMAIN=your-authaction-tenant-domainAUTHACTION_AUDIENCE=your-authaction-api-identifier3. Define the Claims Struct
Section titled “3. Define the Claims Struct”Create src/models.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone)]pub struct Claims { pub sub: String, pub iss: String, pub aud: serde_json::Value, pub exp: u64, pub iat: u64,}4. Implement JWT Validation
Section titled “4. Implement JWT Validation”Create src/auth.rs:
use std::future::Future;use std::pin::Pin;use std::sync::Arc;
use actix_web::{dev::Payload, error, web::Data, FromRequest, HttpRequest};use jsonwebtoken::jwk::JwkSet;use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};use tokio::sync::RwLock;
use crate::models::Claims;
pub struct AppState { jwks_cache: RwLock<Option<Arc<JwkSet>>>, pub domain: String, pub audience: String,}
impl AppState { pub fn new(domain: String, audience: String) -> Self { Self { jwks_cache: RwLock::new(None), domain, audience, } }
pub async fn get_jwks(&self) -> Result<Arc<JwkSet>, String> { { let guard = self.jwks_cache.read().await; if let Some(ref jwks) = *guard { return Ok(Arc::clone(jwks)); } } self.fetch_and_cache_jwks().await }
pub async fn invalidate_and_refetch(&self) -> Result<Arc<JwkSet>, String> { { let mut guard = self.jwks_cache.write().await; *guard = None; } self.fetch_and_cache_jwks().await }
async fn fetch_and_cache_jwks(&self) -> Result<Arc<JwkSet>, String> { let url = format!("https://{}/.well-known/jwks.json", self.domain); let jwks: JwkSet = reqwest::get(&url).await .map_err(|e| e.to_string())? .json().await .map_err(|e| e.to_string())?; let jwks = Arc::new(jwks); let mut guard = self.jwks_cache.write().await; *guard = Some(Arc::clone(&jwks)); Ok(jwks) }}
pub async fn verify_token(token: &str, state: &AppState) -> Result<Claims, String> { let header = decode_header(token).map_err(|e| e.to_string())?; let kid = header.kid.ok_or("Missing kid in token header")?;
let issuer = format!("https://{}", state.domain); let mut validation = Validation::new(Algorithm::RS256); validation.set_issuer(&[&issuer]); validation.set_audience(&[&state.audience]);
// Try cached JWKS; if kid is absent (key rotation), bust cache and retry once. let jwks = state.get_jwks().await?; if let Some(jwk) = jwks.find(&kid) { let key = DecodingKey::from_jwk(jwk).map_err(|e| e.to_string())?; let data = decode::<Claims>(token, &key, &validation).map_err(|e| e.to_string())?; return Ok(data.claims); }
let jwks = state.invalidate_and_refetch().await?; let jwk = jwks.find(&kid).ok_or("Matching public key not found")?; let key = DecodingKey::from_jwk(jwk).map_err(|e| e.to_string())?; let data = decode::<Claims>(token, &key, &validation).map_err(|e| e.to_string())?; Ok(data.claims)}
/// Actix-web extractor that validates the Bearer JWT and injects the decoded claims./// Adding it as a handler parameter automatically protects the route.pub struct AuthenticatedUser { pub claims: Claims,}
impl FromRequest for AuthenticatedUser { type Error = actix_web::Error; type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let req = req.clone(); Box::pin(async move { let state = req .app_data::<Data<AppState>>() .ok_or_else(|| error::ErrorInternalServerError("missing app state"))?;
let auth_header = req.headers().get("Authorization") .and_then(|v| v.to_str().ok()) .ok_or_else(|| error::ErrorUnauthorized("Missing Authorization header"))?;
let token = auth_header.strip_prefix("Bearer ") .map(|t| t.trim()) .ok_or_else(|| error::ErrorUnauthorized("Invalid Authorization header"))?;
let claims = verify_token(token, state) .await .map_err(|e| error::ErrorUnauthorized(e))?;
Ok(AuthenticatedUser { claims }) }) }}1. Create Your Application
Section titled “1. Create Your Application”Create src/main.rs:
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
mod auth;mod models;
use auth::{AppState, AuthenticatedUser};
async fn public_route() -> impl Responder { HttpResponse::Ok().json(serde_json::json!({ "message": "This is a public message!" }))}
async fn protected_route(user: AuthenticatedUser) -> impl Responder { HttpResponse::Ok().json(serde_json::json!({ "message": "This is a protected message!", "sub": user.claims.sub }))}
#[actix_web::main]async fn main() -> std::io::Result<()> { dotenvy::dotenv().ok();
let domain = std::env::var("AUTHACTION_DOMAIN") .expect("AUTHACTION_DOMAIN must be set"); let audience = std::env::var("AUTHACTION_AUDIENCE") .expect("AUTHACTION_AUDIENCE must be set");
let state = web::Data::new(AppState::new(domain, audience));
println!("Server running at http://0.0.0.0:8080");
HttpServer::new(move || { App::new() .app_data(state.clone()) .route("/public", web::get().to(public_route)) .route("/protected", web::get().to(protected_route)) }) .bind(("0.0.0.0", 8080))? .run() .await}2. Build and Run
Section titled “2. Build and Run”cargo runThe API will be available at http://localhost:8080.
3. Testing the API
Section titled “3. Testing the API”- Obtain an Access Token:
curl --request POST \ --url https://your-authaction-tenant-domain/oauth2/m2m/token \ --header 'content-type: application/json' \ --data '{ "client_id": "your-authaction-m2m-app-clientid", "client_secret": "your-authaction-m2m-app-client-secret", "audience": "your-authaction-api-identifier", "grant_type": "client_credentials" }'- Call the Public Endpoint:
curl http://localhost:8080/public- Call the Protected Endpoint:
curl --request GET \ --url http://localhost:8080/protected \ --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'Common Issues
Section titled “Common Issues”Invalid Token Errors
Section titled “Invalid Token Errors”- Verify
AUTHACTION_DOMAINandAUTHACTION_AUDIENCEmatch your AuthAction dashboard exactly - Ensure the token is signed with the RS256 algorithm
Public Key Fetching Errors
Section titled “Public Key Fetching Errors”- Verify your application can reach
https://your-authaction-tenant-domain/.well-known/jwks.json
Unauthorized Access
Section titled “Unauthorized Access”- Ensure the
Authorization: Bearer <token>header is present and the token is not expired - Confirm the token’s audience matches your API identifier