r_token/
lib.rs

1#![deny(clippy::unwrap_used)]
2#![deny(clippy::expect_used)]
3#![deny(clippy::panic)]
4#![deny(clippy::todo)]
5#![deny(clippy::unimplemented)]
6#![deny(clippy::empty_loop)]
7#![deny(clippy::indexing_slicing)]
8#![deny(unused)]
9//! # r-token
10//!
11//! A small, in-memory token authentication helper for actix-web.
12//!
13//! The library exposes two main building blocks:
14//! - [`RTokenManager`]: issues and revokes tokens (UUID v4) and keeps an in-memory store.
15//! - [`RUser`]: an actix-web extractor that validates `Authorization` automatically.
16//!
17//! ## How authentication works
18//!
19//! 1. Your login handler calls [`RTokenManager::login`] with a user id and a TTL (seconds).
20//! 2. The token is returned to the client (typically as plain text or JSON).
21//! 3. The client sends the token back via `Authorization` header:
22//!    - `Authorization: <token>`
23//!    - `Authorization: Bearer <token>`
24//! 4. Any handler that declares an [`RUser`] parameter becomes a protected endpoint. If extraction
25//!    succeeds, the request is considered authenticated; otherwise actix-web returns an error.
26//!
27//! ## 繁體中文
28//!
29//! 這是一個為 actix-web 設計的輕量級、純記憶體 token 驗證輔助庫。
30//!
31//! 主要由兩個元件構成:
32//! - [`RTokenManager`]: 產生/註銷 token(UUID v4),並在記憶體中維護映射表。
33//! - [`RUser`]: actix-web 的 Extractor,會自動從 `Authorization` 讀取並驗證 token。
34//!
35//! ## 驗證流程
36//!
37//! 1. 登入端點呼叫 [`RTokenManager::login`],傳入使用者 id 與 TTL(秒)。
38//! 2. token 回傳給客戶端(常見為純文字或 JSON)。
39//! 3. 客戶端透過 `Authorization` header 送回 token(支援 `Bearer ` 前綴或不帶前綴)。
40//! 4. 任何 handler 只要宣告 [`RUser`] 參數即視為受保護端點;Extractor 成功才會進入 handler。
41
42mod memory;
43mod models;
44#[cfg(feature = "redis")]
45mod redis;
46
47pub const TOKEN_COOKIE_NAME: &str = "r_token";
48
49#[cfg(feature = "actix")]
50#[derive(Clone, Debug)]
51pub enum TokenSourcePriority {
52    HeaderFirst,
53    CookieFirst,
54}
55
56#[cfg(feature = "actix")]
57#[derive(Clone, Debug)]
58pub struct TokenSourceConfig {
59    pub priority: TokenSourcePriority,
60    pub header_names: Vec<String>,
61    pub cookie_names: Vec<String>,
62}
63
64#[cfg(feature = "actix")]
65impl Default for TokenSourceConfig {
66    fn default() -> Self {
67        Self {
68            priority: TokenSourcePriority::HeaderFirst,
69            header_names: vec!["Authorization".to_string()],
70            cookie_names: vec![TOKEN_COOKIE_NAME.to_string(), "token".to_string()],
71        }
72    }
73}
74
75#[cfg(feature = "actix")]
76pub fn extract_token_from_request(req: &actix_web::HttpRequest) -> Option<String> {
77    use actix_web::web;
78
79    if let Some(cfg) = req.app_data::<web::Data<TokenSourceConfig>>() {
80        extract_token_from_request_with_config(req, cfg.as_ref())
81    } else {
82        let default_cfg = TokenSourceConfig::default();
83        extract_token_from_request_with_config(req, &default_cfg)
84    }
85}
86
87#[cfg(feature = "actix")]
88pub fn extract_token_from_request_with_config(
89    req: &actix_web::HttpRequest,
90    cfg: &TokenSourceConfig,
91) -> Option<String> {
92    let from_headers = || {
93        cfg.header_names.iter().find_map(|name| {
94            req.headers()
95                .get(name)
96                .and_then(|h| h.to_str().ok())
97                .map(|token_str| token_str.strip_prefix("Bearer ").unwrap_or(token_str).to_string())
98        })
99    };
100
101    let from_cookies = || {
102        cfg.cookie_names.iter().find_map(|name| {
103            req.cookie(name)
104                .map(|cookie| cookie.value().to_string())
105        })
106    };
107
108    match cfg.priority {
109        TokenSourcePriority::HeaderFirst => from_headers().or_else(from_cookies),
110        TokenSourcePriority::CookieFirst => from_cookies().or_else(from_headers),
111    }
112}
113
114pub use crate::memory::RTokenManager;
115#[cfg(feature = "actix")]
116pub use crate::memory::RUser;
117pub use crate::models::RTokenError;
118#[cfg(feature = "redis")]
119pub use crate::redis::RTokenRedisManager;
120#[cfg(all(feature = "redis", feature = "actix"))]
121pub use crate::redis::RRedisUser;