1use fastapi::core::{
61 App, BearerToken, Request, RequestContext, Response, ResponseBody, SecureCompare, StatusCode,
62 TestClient,
63};
64use serde::Serialize;
65
66const SECRET_TOKEN: &str = "demo_secret_token_12345";
69
70#[derive(Debug, Serialize)]
72struct LoginResponse {
73 access_token: String,
74 token_type: &'static str,
75}
76
77#[derive(Debug, Serialize)]
79struct UserInfo {
80 username: String,
81 message: String,
82}
83
84fn public_handler(_ctx: &RequestContext, _req: &mut Request) -> std::future::Ready<Response> {
88 let body = serde_json::json!({
89 "message": "This is a public endpoint - no authentication required!"
90 });
91 std::future::ready(
92 Response::ok()
93 .header("content-type", b"application/json".to_vec())
94 .body(ResponseBody::Bytes(body.to_string().into_bytes())),
95 )
96}
97
98fn login_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
108 let is_json = req
113 .headers()
114 .get("content-type")
115 .is_some_and(|ct| ct.starts_with(b"application/json"));
116
117 if !is_json {
118 let error = serde_json::json!({
119 "detail": "Content-Type must be application/json"
120 });
121 return std::future::ready(
122 Response::with_status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
123 .header("content-type", b"application/json".to_vec())
124 .body(ResponseBody::Bytes(error.to_string().into_bytes())),
125 );
126 }
127
128 let response = LoginResponse {
136 access_token: SECRET_TOKEN.to_string(),
137 token_type: "bearer",
138 };
139
140 std::future::ready(
141 Response::ok()
142 .header("content-type", b"application/json".to_vec())
143 .body(ResponseBody::Bytes(
144 serde_json::to_string(&response).unwrap().into_bytes(),
145 )),
146 )
147}
148
149fn protected_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
158 let Some(auth_header) = req.headers().get("authorization") else {
160 let body = serde_json::json!({
162 "detail": "Not authenticated"
163 });
164 return std::future::ready(
165 Response::with_status(StatusCode::UNAUTHORIZED)
166 .header("www-authenticate", b"Bearer".to_vec())
167 .header("content-type", b"application/json".to_vec())
168 .body(ResponseBody::Bytes(body.to_string().into_bytes())),
169 );
170 };
171
172 let Ok(auth_str) = std::str::from_utf8(auth_header) else {
174 let body = serde_json::json!({
176 "detail": "Invalid authentication credentials"
177 });
178 return std::future::ready(
179 Response::with_status(StatusCode::UNAUTHORIZED)
180 .header("www-authenticate", b"Bearer".to_vec())
181 .header("content-type", b"application/json".to_vec())
182 .body(ResponseBody::Bytes(body.to_string().into_bytes())),
183 );
184 };
185
186 let Some(token) = auth_str
188 .strip_prefix("Bearer ")
189 .or_else(|| auth_str.strip_prefix("bearer "))
190 else {
191 let body = serde_json::json!({
193 "detail": "Invalid authentication credentials"
194 });
195 return std::future::ready(
196 Response::with_status(StatusCode::UNAUTHORIZED)
197 .header("www-authenticate", b"Bearer".to_vec())
198 .header("content-type", b"application/json".to_vec())
199 .body(ResponseBody::Bytes(body.to_string().into_bytes())),
200 );
201 };
202
203 let token = token.trim();
204 if token.is_empty() {
205 let body = serde_json::json!({
207 "detail": "Invalid authentication credentials"
208 });
209 return std::future::ready(
210 Response::with_status(StatusCode::UNAUTHORIZED)
211 .header("www-authenticate", b"Bearer".to_vec())
212 .header("content-type", b"application/json".to_vec())
213 .body(ResponseBody::Bytes(body.to_string().into_bytes())),
214 );
215 }
216
217 let bearer_token = BearerToken::new(token);
220 if !bearer_token.secure_eq(SECRET_TOKEN) {
221 let body = serde_json::json!({
223 "detail": "Invalid token"
224 });
225 return std::future::ready(
226 Response::with_status(StatusCode::FORBIDDEN)
227 .header("content-type", b"application/json".to_vec())
228 .body(ResponseBody::Bytes(body.to_string().into_bytes())),
229 );
230 }
231
232 let user_info = UserInfo {
234 username: "demo_user".to_string(),
235 message: "You have accessed a protected resource!".to_string(),
236 };
237
238 std::future::ready(
239 Response::ok()
240 .header("content-type", b"application/json".to_vec())
241 .body(ResponseBody::Bytes(
242 serde_json::to_string(&user_info).unwrap().into_bytes(),
243 )),
244 )
245}
246
247#[allow(clippy::too_many_lines)]
248fn main() {
249 println!("fastapi_rust Authentication Example");
250 println!("====================================\n");
251
252 let app = App::builder()
254 .get("/public", public_handler)
256 .post("/login", login_handler)
258 .get("/protected", protected_handler)
260 .build();
261
262 println!("App created with {} route(s)\n", app.route_count());
263
264 let client = TestClient::new(app);
266
267 println!("1. Public endpoint - no auth required");
271 let response = client.get("/public").send();
272 println!(
273 " GET /public -> {} {}",
274 response.status().as_u16(),
275 response.status().canonical_reason()
276 );
277 assert_eq!(response.status().as_u16(), 200);
278 assert!(response.text().contains("public endpoint"));
279
280 println!("\n2. Protected endpoint - without token");
284 let response = client.get("/protected").send();
285 println!(
286 " GET /protected -> {} {}",
287 response.status().as_u16(),
288 response.status().canonical_reason()
289 );
290 assert_eq!(
291 response.status().as_u16(),
292 401,
293 "Protected endpoint should return 401 without token"
294 );
295
296 let has_www_auth = response
298 .headers()
299 .iter()
300 .any(|(name, value)| name == "www-authenticate" && value == b"Bearer");
301 assert!(
302 has_www_auth,
303 "401 response should include WWW-Authenticate: Bearer header"
304 );
305
306 println!("\n3. Login endpoint - get a token");
310 let response = client
311 .post("/login")
312 .header("content-type", "application/json")
313 .body(r#"{"username":"test","password":"test123"}"#)
314 .send();
315 println!(
316 " POST /login -> {} {}",
317 response.status().as_u16(),
318 response.status().canonical_reason()
319 );
320 assert_eq!(response.status().as_u16(), 200);
321
322 let body: serde_json::Value = serde_json::from_str(response.text()).unwrap();
324 let token = body["access_token"].as_str().unwrap();
325 println!(" Token: {token}");
326 assert_eq!(token, SECRET_TOKEN);
327
328 println!("\n4. Protected endpoint - with valid token");
332 let response = client
333 .get("/protected")
334 .header("authorization", format!("Bearer {SECRET_TOKEN}"))
335 .send();
336 println!(
337 " GET /protected (Authorization: Bearer {}) -> {} {}",
338 SECRET_TOKEN,
339 response.status().as_u16(),
340 response.status().canonical_reason()
341 );
342 assert_eq!(
343 response.status().as_u16(),
344 200,
345 "Protected endpoint should return 200 with valid token"
346 );
347 assert!(response.text().contains("protected resource"));
348
349 println!("\n5. Protected endpoint - with invalid token");
353 let response = client
354 .get("/protected")
355 .header("authorization", "Bearer wrong_token")
356 .send();
357 println!(
358 " GET /protected (Authorization: Bearer wrong_token) -> {} {}",
359 response.status().as_u16(),
360 response.status().canonical_reason()
361 );
362 assert_eq!(
363 response.status().as_u16(),
364 403,
365 "Protected endpoint should return 403 with invalid token"
366 );
367
368 println!("\n6. Protected endpoint - with wrong auth scheme");
372 let response = client
373 .get("/protected")
374 .header("authorization", "Basic dXNlcjpwYXNz")
375 .send();
376 println!(
377 " GET /protected (Authorization: Basic ...) -> {} {}",
378 response.status().as_u16(),
379 response.status().canonical_reason()
380 );
381 assert_eq!(
382 response.status().as_u16(),
383 401,
384 "Protected endpoint should return 401 with wrong auth scheme"
385 );
386
387 println!("\n7. Login with wrong Content-Type");
391 let response = client
392 .post("/login")
393 .header("content-type", "text/plain")
394 .body("username=test&password=test123")
395 .send();
396 println!(
397 " POST /login (Content-Type: text/plain) -> {} {}",
398 response.status().as_u16(),
399 response.status().canonical_reason()
400 );
401 assert_eq!(
402 response.status().as_u16(),
403 415,
404 "Login should return 415 with wrong Content-Type"
405 );
406
407 println!("\n8. Token case sensitivity (lowercase 'bearer')");
411 let response = client
412 .get("/protected")
413 .header("authorization", format!("bearer {SECRET_TOKEN}"))
414 .send();
415 println!(
416 " GET /protected (Authorization: bearer {}) -> {} {}",
417 SECRET_TOKEN,
418 response.status().as_u16(),
419 response.status().canonical_reason()
420 );
421 assert_eq!(
422 response.status().as_u16(),
423 200,
424 "Bearer scheme should be case-insensitive (lowercase accepted)"
425 );
426
427 println!("\nAll authentication tests passed!");
428}