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;