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
47/// Default cookie name used for token transport.
48///
49/// This name is used by:
50/// - the example servers when setting cookies on `/login`
51/// - the actix extractors when reading the token from cookies
52///
53/// ## 繁體中文
54///
55/// 預設的 token Cookie 名稱。
56///
57/// 這個名稱會被用在:
58/// - 範例伺服器的 `/login` 回應中設定 Cookie
59/// - actix Extractor 從 Cookie 讀取 token
60pub const TOKEN_COOKIE_NAME: &str = "r_token";
61
62#[cfg(feature = "actix")]
63#[derive(Clone, Debug)]
64/// Priority for selecting which token source to use when multiple are present.
65///
66/// ## 繁體中文
67///
68/// 當同時存在多種 token 來源時,選擇使用哪一種的優先順序。
69pub enum TokenSourcePriority {
70    /// Prefer headers (e.g. `Authorization`) over cookies.
71    ///
72    /// ## 繁體中文
73    ///
74    /// Header(例如 `Authorization`)優先於 Cookie。
75    HeaderFirst,
76    /// Prefer cookies over headers.
77    ///
78    /// ## 繁體中文
79    ///
80    /// Cookie 優先於 Header。
81    CookieFirst,
82}
83
84#[cfg(feature = "actix")]
85#[derive(Clone, Debug)]
86/// Token source configuration for actix extractors.
87///
88/// You can register this as `app_data(web::Data<TokenSourceConfig>)` to customize:
89/// - which header names are scanned for a token
90/// - which cookie names are scanned for a token
91/// - the priority order between header/cookie
92///
93/// ## 繁體中文
94///
95/// actix Extractor 的 token 來源設定。
96///
97/// 你可以把它以 `app_data(web::Data<TokenSourceConfig>)` 的形式注入到 App,
98/// 用來自訂:
99/// - 會掃描哪些 header 名稱來取得 token
100/// - 會掃描哪些 cookie 名稱來取得 token
101/// - header/cookie 之間的優先順序
102pub struct TokenSourceConfig {
103    /// Priority of token sources.
104    ///
105    /// ## 繁體中文
106    ///
107    /// token 來源優先順序。
108    pub priority: TokenSourcePriority,
109    /// Header names that will be checked in order.
110    ///
111    /// ## 繁體中文
112    ///
113    /// 會依序檢查的 header 名稱列表。
114    pub header_names: Vec<String>,
115    /// Cookie names that will be checked in order.
116    ///
117    /// ## 繁體中文
118    ///
119    /// 會依序檢查的 cookie 名稱列表。
120    pub cookie_names: Vec<String>,
121}
122
123#[cfg(feature = "actix")]
124impl Default for TokenSourceConfig {
125    fn default() -> Self {
126        Self {
127            priority: TokenSourcePriority::HeaderFirst,
128            header_names: vec!["Authorization".to_string()],
129            cookie_names: vec![TOKEN_COOKIE_NAME.to_string(), "token".to_string()],
130        }
131    }
132}
133
134#[cfg(feature = "actix")]
135/// Extracts a token from an actix-web request.
136///
137/// The function reads configuration from `app_data(web::Data<TokenSourceConfig>)` if present;
138/// otherwise it falls back to `TokenSourceConfig::default()`.
139///
140/// ## 繁體中文
141///
142/// 從 actix-web 請求中抽取 token。
143///
144/// 若 App 有注入 `app_data(web::Data<TokenSourceConfig>)` 則會使用該設定;
145/// 否則使用 `TokenSourceConfig::default()`。
146pub fn extract_token_from_request(req: &actix_web::HttpRequest) -> Option<String> {
147    use actix_web::web;
148
149    if let Some(cfg) = req.app_data::<web::Data<TokenSourceConfig>>() {
150        extract_token_from_request_with_config(req, cfg.as_ref())
151    } else {
152        let default_cfg = TokenSourceConfig::default();
153        extract_token_from_request_with_config(req, &default_cfg)
154    }
155}
156
157#[cfg(feature = "actix")]
158/// Extracts a token from an actix-web request using an explicit config.
159///
160/// Token parsing behavior:
161/// - Header values support both `Bearer <token>` and raw `<token>` formats.
162/// - Cookie values use the raw cookie value as the token.
163///
164/// ## 繁體中文
165///
166/// 使用指定設定,從 actix-web 請求中抽取 token。
167///
168/// 解析行為:
169/// - Header 支援 `Bearer <token>` 與純 `<token>` 兩種格式。
170/// - Cookie 直接使用 cookie value 作為 token。
171pub fn extract_token_from_request_with_config(
172    req: &actix_web::HttpRequest,
173    cfg: &TokenSourceConfig,
174) -> Option<String> {
175    let from_headers = || {
176        cfg.header_names.iter().find_map(|name| {
177            req.headers()
178                .get(name)
179                .and_then(|h| h.to_str().ok())
180                .map(|token_str| {
181                    token_str
182                        .strip_prefix("Bearer ")
183                        .unwrap_or(token_str)
184                        .to_string()
185                })
186        })
187    };
188
189    let from_cookies = || {
190        cfg.cookie_names
191            .iter()
192            .find_map(|name| req.cookie(name).map(|cookie| cookie.value().to_string()))
193    };
194
195    match cfg.priority {
196        TokenSourcePriority::HeaderFirst => from_headers().or_else(from_cookies),
197        TokenSourcePriority::CookieFirst => from_cookies().or_else(from_headers),
198    }
199}
200
201pub use crate::memory::RTokenManager;
202#[cfg(feature = "actix")]
203pub use crate::memory::RUser;
204pub use crate::models::RTokenError;
205#[cfg(all(feature = "redis", feature = "actix"))]
206pub use crate::redis::RRedisUser;
207#[cfg(feature = "redis")]
208pub use crate::redis::RTokenRedisManager;