Skip to content

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.

Before you begin, ensure you have:

  1. Rust 1.75+: Install via rustup
  2. AuthAction Account: You’ll need your AuthAction tenant domain and API identifier

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"

Create a .env file in your project root:

Terminal window
AUTHACTION_DOMAIN=your-authaction-tenant-domain
AUTHACTION_AUDIENCE=your-authaction-api-identifier

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,
}

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 })
})
}
}

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
}
Terminal window
cargo run

The API will be available at http://localhost:8080.

  1. Obtain an Access Token:
Terminal window
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"
}'
  1. Call the Public Endpoint:
Terminal window
curl http://localhost:8080/public
  1. Call the Protected Endpoint:
Terminal window
curl --request GET \
--url http://localhost:8080/protected \
--header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
  • Verify AUTHACTION_DOMAIN and AUTHACTION_AUDIENCE match your AuthAction dashboard exactly
  • Ensure the token is signed with the RS256 algorithm
  • Verify your application can reach https://your-authaction-tenant-domain/.well-known/jwks.json
  • Ensure the Authorization: Bearer <token> header is present and the token is not expired
  • Confirm the token’s audience matches your API identifier