Skip to main content

auth_example/
auth_example.rs

1//! Authentication Example - Bearer Token Authentication with Protected Routes
2//!
3//! This example demonstrates authentication patterns in fastapi_rust:
4//! - Bearer token authentication
5//! - Protected routes that return 401 without valid token
6//! - A simulated login endpoint
7//! - Public and private endpoints
8//! - Secure token comparison to prevent timing attacks
9//!
10//! # Running This Example
11//!
12//! ```bash
13//! cargo run --example auth_example
14//! ```
15//!
16//! # Expected Output
17//!
18//! ```text
19//! fastapi_rust Authentication Example
20//! ====================================
21//!
22//! 1. Public endpoint - no auth required
23//!    GET /public -> 200 OK
24//!
25//! 2. Protected endpoint - without token
26//!    GET /protected -> 401 Unauthorized
27//!
28//! 3. Login endpoint - get a token
29//!    POST /login -> 200 OK
30//!    Token: demo_secret_token_12345
31//!
32//! 4. Protected endpoint - with valid token
33//!    GET /protected (Authorization: Bearer demo_secret_token_12345) -> 200 OK
34//!
35//! 5. Protected endpoint - with invalid token
36//!    GET /protected (Authorization: Bearer wrong_token) -> 403 Forbidden
37//!
38//! 6. Protected endpoint - with wrong auth scheme
39//!    GET /protected (Authorization: Basic ...) -> 401 Unauthorized
40//!
41//! 7. Login with wrong Content-Type
42//!    POST /login (Content-Type: text/plain) -> 415 Unsupported Media Type
43//!
44//! 8. Token case sensitivity (lowercase 'bearer')
45//!    GET /protected (Authorization: bearer demo_secret_token_12345) -> 200 OK
46//!
47//! All authentication tests passed!
48//! ```
49//!
50//! # Security Notes
51//!
52//! This example uses a hardcoded secret token for demonstration purposes.
53//! In a production application:
54//! - Use cryptographically secure random tokens (e.g., UUID v4 or JWT)
55//! - Store tokens securely (hashed in database)
56//! - Implement token expiration
57//! - Use HTTPS to protect tokens in transit
58//! - Consider using OAuth2 or JWT for more complex scenarios
59
60use fastapi::core::{
61    App, BearerToken, Request, RequestContext, Response, ResponseBody, SecureCompare, StatusCode,
62    TestClient,
63};
64use serde::Serialize;
65
66/// The secret token used for authentication in this demo.
67/// In production, this would be generated per-user and stored securely.
68const SECRET_TOKEN: &str = "demo_secret_token_12345";
69
70/// Login response body.
71#[derive(Debug, Serialize)]
72struct LoginResponse {
73    access_token: String,
74    token_type: &'static str,
75}
76
77/// User info returned from protected endpoints.
78#[derive(Debug, Serialize)]
79struct UserInfo {
80    username: String,
81    message: String,
82}
83
84/// Handler for public endpoint - accessible without authentication.
85///
86/// This endpoint demonstrates a route that anyone can access.
87fn 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
98/// Handler for the login endpoint.
99///
100/// In a real application, this would:
101/// 1. Validate username/password against a database
102/// 2. Generate a unique token (JWT or random)
103/// 3. Store the token with associated user info
104/// 4. Return the token to the client
105///
106/// For this demo, we accept any credentials and return a fixed token.
107fn login_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
108    // In a real app, we would parse the JSON body and validate credentials.
109    // For this demo, we just check that it's a POST with some body.
110
111    // Check Content-Type
112    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    // For demo purposes, we don't validate credentials - just return the token
129    // In production, you would:
130    // 1. Parse the request body as LoginRequest
131    // 2. Verify username/password against your database
132    // 3. Generate a unique, cryptographically secure token
133    // 4. Store token -> user_id mapping (with expiration)
134
135    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
149/// Handler for protected endpoint - requires valid bearer token.
150///
151/// This handler manually extracts and validates the bearer token:
152/// 1. Gets the Authorization header
153/// 2. Verifies it uses the Bearer scheme
154/// 3. Validates the token against our secret using constant-time comparison
155///
156/// Returns appropriate error responses for each failure mode.
157fn protected_handler(_ctx: &RequestContext, req: &mut Request) -> std::future::Ready<Response> {
158    // Step 1: Get the Authorization header
159    let Some(auth_header) = req.headers().get("authorization") else {
160        // Missing header -> 401 Unauthorized
161        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    // Step 2: Parse the Authorization header
173    let Ok(auth_str) = std::str::from_utf8(auth_header) else {
174        // Invalid UTF-8 -> 401 Unauthorized
175        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    // Step 3: Check for "Bearer " prefix (case-insensitive for the scheme)
187    let Some(token) = auth_str
188        .strip_prefix("Bearer ")
189        .or_else(|| auth_str.strip_prefix("bearer "))
190    else {
191        // Wrong scheme -> 401 Unauthorized
192        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        // Empty token -> 401 Unauthorized
206        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    // Step 4: Validate the token using constant-time comparison
218    // Create a BearerToken for secure comparison
219    let bearer_token = BearerToken::new(token);
220    if !bearer_token.secure_eq(SECRET_TOKEN) {
221        // Invalid token -> 403 Forbidden
222        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    // Token is valid - return protected data
233    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    // Build the application with public and protected routes
253    let app = App::builder()
254        // Public endpoints - accessible to everyone
255        .get("/public", public_handler)
256        // Login endpoint - returns a token
257        .post("/login", login_handler)
258        // Protected endpoint - requires valid bearer token
259        .get("/protected", protected_handler)
260        .build();
261
262    println!("App created with {} route(s)\n", app.route_count());
263
264    // Create a test client
265    let client = TestClient::new(app);
266
267    // =========================================================================
268    // Test 1: Public endpoint - no auth required
269    // =========================================================================
270    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    // =========================================================================
281    // Test 2: Protected endpoint - without token (should get 401)
282    // =========================================================================
283    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    // Check for WWW-Authenticate header
297    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    // =========================================================================
307    // Test 3: Login endpoint - get a token
308    // =========================================================================
309    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    // Parse the response to get the token
323    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    // =========================================================================
329    // Test 4: Protected endpoint - with valid token (should get 200)
330    // =========================================================================
331    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    // =========================================================================
350    // Test 5: Protected endpoint - with invalid token (should get 403)
351    // =========================================================================
352    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    // =========================================================================
369    // Test 6: Protected endpoint - with wrong auth scheme (should get 401)
370    // =========================================================================
371    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    // =========================================================================
388    // Test 7: Login with wrong Content-Type (should get 415)
389    // =========================================================================
390    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    // =========================================================================
408    // Test 8: Token case sensitivity (lowercase 'bearer')
409    // =========================================================================
410    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}