use fastapi_rust::core::{
App, Request, RequestContext, Response, ResponseBody, SecureCompare, StatusCode, TestClient,
};
use serde::Serialize;
const DEMO_BEARER_VALUE: &str = "demo-bearer-value";
#[derive(Debug, Serialize)]
struct LoginResponse {
access_token: String,
token_type: &'static str,
}
#[derive(Debug, Serialize)]
struct UserInfo {
username: String,
message: String,
}
fn public_handler(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
let body = serde_json::json!({
"message": "This is a public endpoint - no authentication required!"
});
std::future::ready(
Response::ok()
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.to_string().into_bytes())),
)
}
fn login_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
let is_json = req
.headers()
.get("content-type")
.is_some_and(|ct| ct.starts_with(b"application/json"));
if !is_json {
let error = serde_json::json!({
"detail": "Content-Type must be application/json"
});
return std::future::ready(
Response::with_status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(error.to_string().into_bytes())),
);
}
let response = LoginResponse {
access_token: DEMO_BEARER_VALUE.to_string(),
token_type: "bearer",
};
std::future::ready(
Response::ok()
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(json_bytes(&response))),
)
}
fn protected_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
let Some(auth_header) = req.headers().get("authorization") else {
let body = serde_json::json!({
"detail": "Not authenticated"
});
return std::future::ready(
Response::with_status(StatusCode::UNAUTHORIZED)
.header("www-authenticate", b"Bearer".to_vec())
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.to_string().into_bytes())),
);
};
let Ok(auth_str) = std::str::from_utf8(auth_header) else {
let body = serde_json::json!({
"detail": "Invalid authentication credentials"
});
return std::future::ready(
Response::with_status(StatusCode::UNAUTHORIZED)
.header("www-authenticate", b"Bearer".to_vec())
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.to_string().into_bytes())),
);
};
let Some(bearer_value) = auth_str
.strip_prefix("Bearer ")
.or_else(|| auth_str.strip_prefix("bearer "))
else {
let body = serde_json::json!({
"detail": "Invalid authentication credentials"
});
return std::future::ready(
Response::with_status(StatusCode::UNAUTHORIZED)
.header("www-authenticate", b"Bearer".to_vec())
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.to_string().into_bytes())),
);
};
let bearer_value = bearer_value.trim();
if bearer_value.is_empty() {
let body = serde_json::json!({
"detail": "Invalid authentication credentials"
});
return std::future::ready(
Response::with_status(StatusCode::UNAUTHORIZED)
.header("www-authenticate", b"Bearer".to_vec())
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.to_string().into_bytes())),
);
}
if !bearer_value.secure_eq(DEMO_BEARER_VALUE) {
let body = serde_json::json!({
"detail": "Invalid token"
});
return std::future::ready(
Response::with_status(StatusCode::FORBIDDEN)
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(body.to_string().into_bytes())),
);
}
let user_info = UserInfo {
username: "demo_user".to_string(),
message: "You have accessed a protected resource!".to_string(),
};
std::future::ready(
Response::ok()
.header("content-type", b"application/json".to_vec())
.body(ResponseBody::Bytes(json_bytes(&user_info))),
)
}
fn json_bytes<T: Serialize>(value: &T) -> Vec<u8> {
match serde_json::to_string(value) {
Ok(text) => text.into_bytes(),
Err(err) => format!(r#"{{"detail":"json serialize error: {err}"}}"#).into_bytes(),
}
}
fn check(condition: bool, message: &str) -> bool {
if condition {
true
} else {
eprintln!("Check failed: {message}");
false
}
}
#[allow(clippy::needless_pass_by_value)]
fn check_eq<T: PartialEq + std::fmt::Debug>(left: T, right: T, message: &str) -> bool {
if left == right {
true
} else {
eprintln!("Check failed: {message}. left={left:?} right={right:?}");
false
}
}
#[allow(clippy::too_many_lines)]
fn main() {
println!("fastapi_rust Authentication Example");
println!("====================================\n");
let app = App::builder()
.get("/public", public_handler)
.post("/login", login_handler)
.get("/protected", protected_handler)
.build();
println!("App created with {} route(s)\n", app.route_count());
let client = TestClient::new(app);
println!("1. Public endpoint - no auth required");
let response = client.get("/public").send();
println!(
" GET /public -> {} {}",
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
200,
"GET /public should return 200",
) {
return;
}
if !check(
response.text().contains("public endpoint"),
"GET /public should include the public endpoint body",
) {
return;
}
println!("\n2. Protected endpoint - without token");
let response = client.get("/protected").send();
println!(
" GET /protected -> {} {}",
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
401,
"Protected endpoint should return 401 without token",
) {
return;
}
let has_www_auth = response
.headers()
.iter()
.any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
if !check(
has_www_auth,
"401 response should include WWW-Authenticate: Bearer header",
) {
return;
}
println!("\n3. Login endpoint - get a token");
let response = client
.post("/login")
.header("content-type", "application/json")
.body(r#"{"username":"test","password":"test123"}"#)
.send();
println!(
" POST /login -> {} {}",
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
200,
"POST /login should return 200",
) {
return;
}
let body_text = response.text();
let body: serde_json::Value = match serde_json::from_str(body_text) {
Ok(body) => body,
Err(err) => {
eprintln!("Failed to parse login response JSON: {err}");
return;
}
};
let Some(bearer_value) = body.get("access_token").and_then(|value| value.as_str()) else {
eprintln!("Login response missing access_token");
return;
};
println!(" Bearer value: {bearer_value}");
if !check_eq(
bearer_value,
DEMO_BEARER_VALUE,
"Login should return the expected bearer value",
) {
return;
}
println!("\n4. Protected endpoint - with valid token");
let response = client
.get("/protected")
.header("authorization", format!("Bearer {DEMO_BEARER_VALUE}"))
.send();
println!(
" GET /protected (Authorization: Bearer {}) -> {} {}",
DEMO_BEARER_VALUE,
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
200,
"Protected endpoint should return 200 with valid token",
) {
return;
}
if !check(
response.text().contains("protected resource"),
"Protected endpoint should include protected resource body",
) {
return;
}
println!("\n5. Protected endpoint - with invalid token");
let response = client
.get("/protected")
.header("authorization", "Bearer wrong_token")
.send();
println!(
" GET /protected (Authorization: Bearer wrong_token) -> {} {}",
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
403,
"Protected endpoint should return 403 with invalid token",
) {
return;
}
println!("\n6. Protected endpoint - with wrong auth scheme");
let response = client
.get("/protected")
.header("authorization", "Basic dXNlcjpwYXNz")
.send();
println!(
" GET /protected (Authorization: Basic ...) -> {} {}",
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
401,
"Protected endpoint should return 401 with wrong auth scheme",
) {
return;
}
println!("\n7. Login with wrong Content-Type");
let response = client
.post("/login")
.header("content-type", "text/plain")
.body("demo=true")
.send();
println!(
" POST /login (Content-Type: text/plain) -> {} {}",
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
415,
"Login should return 415 with wrong Content-Type",
) {
return;
}
println!("\n8. Token case sensitivity (lowercase 'bearer')");
let response = client
.get("/protected")
.header("authorization", format!("bearer {DEMO_BEARER_VALUE}"))
.send();
println!(
" GET /protected (Authorization: bearer {}) -> {} {}",
DEMO_BEARER_VALUE,
response.status().as_u16(),
response.status().canonical_reason()
);
if !check_eq(
response.status().as_u16(),
200,
"Bearer scheme should be case-insensitive (lowercase accepted)",
) {
return;
}
println!("\nAll authentication tests passed!");
}