---
title: "Add JWT Authentication to Your Axum API"
description: "Secure your Axum API using JWT authentication with JWKS."
canonicalUrl: "https://zuplo.com/use-cases/api-key-auth/rust/axum/jwt-backend"
framework: "Axum"
language: "Rust"
authStrategy: "JWT with JWKS"
pageType: use-case
---

# Add JWT Authentication to Your Axum API

Secure your Axum API using JWT authentication with JWKS.

## How Zuplo Handles It

Let Zuplo issue short-lived JWTs signed with a JWKS your Axum backend can verify — no long-lived API keys touch your origin.

## Axum Backend Code

```rust
use axum::{
    extract::{FromRequest, RequestParts},
    http::{StatusCode, header::AUTHORIZATION},
    response::{IntoResponse, Response},
    routing::get,
    Json, Router,
};
use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock;

const ISSUER: &str = "https://my-api-a32f34.zuplo.api/__zuplo/issuer";
const JWKS_URL: &str = "https://my-api-a32f34.zuplo.api/__zuplo/issuer/.well-known/jwks.json";

#[derive(Serialize, Deserialize)]
struct Claims {
    sub: String,
    // Add additional fields needed from the token payload
}

async fn fetch_jwks() -> Result<HashMap<String, String>, Box<dyn std::error::Error>> {
    let res = reqwest::get(JWKS_URL).await?.json::<Value>().await?;
    let keys = res["keys"].as_array().unwrap();
    let mut jwks = HashMap::new();
    for key in keys {
        let kid = key["kid"].as_str().unwrap().to_owned();
        let n = key["n"].as_str().unwrap();
        jwks.insert(kid, n.to_owned());
    }
    Ok(jwks)
}

struct Jwks(Arc<RwLock<HashMap<String, String>>>);

#[axum::async_trait]
impl<B> FromRequest<B> for Claims
where
    B: Send,
{
    type Rejection = Response;

    async fn from_request(req: RequestParts<B>) -> Result<Self, Self::Rejection> {
        let jwks = req
            .extensions()
            .and_then(|x| x.get::<Jwks>())
            .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?;

        let auth = req
            .headers()
            .get(AUTHORIZATION)
            .and_then(|val| val.to_str().ok())
            .and_then(|val| val.strip_prefix("Bearer "))
            .ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?;

        let header = decode_header(auth).map_err(|_| StatusCode::UNAUTHORIZED)?;

        let kid = header.kid.ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?;

        let jwks = jwks.0.read().await;

        let n = jwks.get(&kid).ok_or_else(|| StatusCode::UNAUTHORIZED.into_response())?;

        let decoding_key = DecodingKey::from_rsa_components(n, "AQAB"); // adjust as necessary

        let mut validation = Validation::new(Algorithm::RS256);
        validation.set_issuer(&[ISSUER]);

        let token_data = decode::<Claims>(auth, &decoding_key, &validation)
            .map_err(|_| StatusCode::UNAUTHORIZED)?;

        Ok(token_data.claims)
    }
}

async fn protected_handler(claims: Claims) -> impl IntoResponse {
    Json(claims)
}

#[tokio::main]
async fn main() {
    let jwks = Jwks(Arc::new(RwLock::new(fetch_jwks().await.unwrap())));

    let app = Router::new()
        .route("/protected", get(protected_handler))
        .layer(axum::AddExtensionLayer::new(jwks));

    println!("Listening on http://localhost:3000");
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}
```

## Example Request

```bash
curl -X GET \
  'https://your-api.zuplo.dev/your-route' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

## Learn More

- [API Key Authentication on Zuplo](https://zuplo.com/docs/policies/api-key-auth-inbound)
- [JWT Authentication on Zuplo](https://zuplo.com/docs/policies/open-id-jwt-auth-inbound)
- [All use cases](https://zuplo.com/use-cases)
