JWT Authentication
Stateless API authentication.
Learning Objectives
- Verify JWT tokens in middleware
##Proving who you are on every request
HTTP is stateless, so the server must identify the caller on every request. One popular approach for APIs is the JSON Web Token (JWT): after login the server gives the client a signed token, and the client sends it back with each request.
#What is inside a JWT
A JWT has three parts separated by dots: a header, a payload of claims and a signature. The claims might say who the user is and when the token expires:
{ "sub": "42", "role": "admin", "exp": 1767225600 }The server signs the token with a secret key. Anyone can read the payload (it is only encoded, not encrypted), but nobody can change it without breaking the signature. So never put secrets in a JWT.
#The login flow
- The user sends a username and password.
- The server verifies the password against a stored hash (use Argon2, never plain text).
- The server creates a JWT with an expiry time and returns it.
- The client sends
Authorization: Bearer <token>with later requests. - The server verifies the signature and expiry, then trusts the claims.
#Creating and verifying tokens
jsonwebtoken = "9"
serde = { version = "1", features = ["derive"] }use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub sub: String,
pub exp: usize,
}
pub fn create_token(user_id: &str, secret: &str) -> Result<String, jsonwebtoken::errors::Error> {
let exp = (chrono::Utc::now() + chrono::Duration::hours(1)).timestamp() as usize;
let claims = Claims { sub: user_id.to_owned(), exp };
encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
}
pub fn verify_token(token: &str, secret: &str) -> Result<Claims, jsonwebtoken::errors::Error> {
decode::<Claims>(
token,
&DecodingKey::from_secret(secret.as_bytes()),
&Validation::default(),
)
.map(|data| data.claims)
}Validation::default() checks the signature and the exp claim for you.
#Protecting routes with an extractor
A neat way to require authentication is a custom extractor. Any handler that asks for AuthUser is automatically protected:
use actix_web::{dev::Payload, error, web, Error, FromRequest, HttpRequest};
use std::future::{ready, Ready};
pub struct AuthUser(pub String);
impl FromRequest for AuthUser {
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let secret = req.app_data::<web::Data<String>>().map(|s| s.as_str()).unwrap_or("");
let result = req
.headers()
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| h.strip_prefix("Bearer "))
.ok_or_else(|| error::ErrorUnauthorized("missing token"))
.and_then(|t| {
verify_token(t, secret).map_err(|_| error::ErrorUnauthorized("invalid token"))
})
.map(|claims| AuthUser(claims.sub));
ready(result)
}
}#[actix_web::get("/me")]
async fn me(user: AuthUser) -> String {
format!("Hello, user {}", user.0)
}#Security checklist
- Keep the secret in an environment variable, long and random.
- Use short expiry times, and refresh tokens for long sessions.
- Always use HTTPS so tokens cannot be sniffed.
- Hash passwords with Argon2 and never log tokens.
- Return 401 for missing or invalid tokens and 403 when the user is known but not allowed.
"What If?" Experiment
Learning by doing. Modify the code to see what happens.
Call the protected route with and without a token.
Repeat with -H "Authorization: Bearer <token>".
The first call returns 401, the second returns the greeting.
Explanation: The extractor is the gatekeeper: only requests with a valid token reach the handler.
Common Mistakes
Anticipate and avoid these frequent pitfalls when working with this concept.
Storing the signing secret in code
Hard-coding it is quick, and then it ends up in version control.
Explanation: Anyone with the secret can forge tokens. Load it from the environment and rotate it if it leaks.
Tokens that never expire
Omitting exp is easier than designing refresh logic.
Explanation: A stolen token without expiry works forever. Short expiry limits the damage.
Practical Exercise
Protect a route
TaskCreate GET /me that returns the user id from a valid token and 401 otherwise.
Requirements
- Implement or reuse the AuthUser extractor
- Return the subject from the token
- Return 401 with no token
Need a hint?
Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.
Real-World Application
SaaS backends issue short-lived access tokens, keep refresh tokens in a database for revocation, and use extractors or middleware so every protected endpoint declares its required identity in its signature.
Key Takeaways
- A JWT is signed, not encrypted; never put secrets in the claims.
- Always set an exp claim and verify it on every request.
- A custom FromRequest extractor makes protected handlers trivial.
- Return 401 for missing or invalid tokens and 403 for insufficient permissions.
Finished reading?
Mark this lesson as done to track your progress and earn XP.