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//! ## 日本語
12//!
13//! actix-web 向けの軽量なインメモリ token 認証ヘルパーです。
14//!
15//! このライブラリは主に次の 2 つを提供します:
16//! - [`RTokenManager`]: token(UUID v4)の発行/失効と、インメモリストアの管理
17//! - [`RUser`]: `Authorization` を自動検証する actix-web extractor
18//!
19//! ## 認証の流れ
20//!
21//! 1. ログイン処理で [`RTokenManager::login`] を呼び、ユーザー ID と TTL(秒)を渡します。
22//! 2. token をクライアントへ返します(多くはプレーンテキストまたは JSON)。
23//! 3. クライアントは `Authorization` header で token を送ります:
24//!    - `Authorization: <token>`
25//!    - `Authorization: Bearer <token>`
26//! 4. handler が [`RUser`] を引数に持つと保護されたエンドポイントになります。抽出が成功すれば認証済みとして扱われ、失敗すれば actix-web がエラーを返します。
27//!
28//! ## English
29//!
30//! A small, in-memory token authentication helper for actix-web.
31//!
32//! The library exposes two main building blocks:
33//! - [`RTokenManager`]: issues and revokes tokens (UUID v4) and keeps an in-memory store.
34//! - [`RUser`]: an actix-web extractor that validates `Authorization` automatically.
35//!
36//! ## How authentication works
37//!
38//! 1. Your login handler calls [`RTokenManager::login`] with a user id and a TTL (seconds).
39//! 2. The token is returned to the client (typically as plain text or JSON).
40//! 3. The client sends the token back via `Authorization` header:
41//!    - `Authorization: <token>`
42//!    - `Authorization: Bearer <token>`
43//! 4. Any handler that declares an [`RUser`] parameter becomes a protected endpoint. If extraction
44//!    succeeds, the request is considered authenticated; otherwise actix-web returns an error.
45
46mod memory;
47mod models;
48#[cfg(feature = "redis")]
49mod redis;
50
51/// ## 日本語
52///
53/// token 送受信に使うデフォルトの Cookie 名です。
54///
55/// この名前は次で使用されます:
56/// - 例のサーバーが `/login` で Cookie をセットするとき
57/// - actix extractor が Cookie から token を読むとき
58///
59/// ## English
60///
61/// Default cookie name used for token transport.
62///
63/// This name is used by:
64/// - the example servers when setting cookies on `/login`
65/// - the actix extractors when reading the token from cookies
66pub const TOKEN_COOKIE_NAME: &str = "r_token";
67
68#[cfg(feature = "actix")]
69#[derive(Clone, Debug)]
70/// ## 日本語
71///
72/// 複数の token 供給元がある場合に、どちらを優先するかの設定です。
73///
74/// ## English
75///
76/// Priority for selecting which token source to use when multiple are present.
77pub enum TokenSourcePriority {
78    /// ## 日本語
79    ///
80    /// Header(例:`Authorization`)を Cookie より優先します。
81    ///
82    /// ## English
83    ///
84    /// Prefer headers (e.g. `Authorization`) over cookies.
85    HeaderFirst,
86    /// ## 日本語
87    ///
88    /// Cookie を header より優先します。
89    ///
90    /// ## English
91    ///
92    /// Prefer cookies over headers.
93    CookieFirst,
94}
95
96#[cfg(feature = "actix")]
97#[derive(Clone, Debug)]
98/// ## 日本語
99///
100/// actix extractor の token 取得元を設定します。
101///
102/// `app_data(web::Data<TokenSourceConfig>)` として登録すると、次をカスタマイズできます:
103/// - どの header 名を順に探索するか
104/// - どの cookie 名を順に探索するか
105/// - header/cookie の優先順位
106///
107/// ## English
108///
109/// Token source configuration for actix extractors.
110///
111/// You can register this as `app_data(web::Data<TokenSourceConfig>)` to customize:
112/// - which header names are scanned for a token
113/// - which cookie names are scanned for a token
114/// - the priority order between header/cookie
115pub struct TokenSourceConfig {
116    /// ## 日本語
117    ///
118    /// token 取得元の優先順位。
119    ///
120    /// ## English
121    ///
122    /// Priority of token sources.
123    pub priority: TokenSourcePriority,
124    /// ## 日本語
125    ///
126    /// 順にチェックする header 名の一覧。
127    ///
128    /// ## English
129    ///
130    /// Header names that will be checked in order.
131    pub header_names: Vec<String>,
132    /// ## 日本語
133    ///
134    /// 順にチェックする cookie 名の一覧。
135    ///
136    /// ## English
137    ///
138    /// Cookie names that will be checked in order.
139    pub cookie_names: Vec<String>,
140}
141
142#[cfg(feature = "actix")]
143impl Default for TokenSourceConfig {
144    fn default() -> Self {
145        Self {
146            priority: TokenSourcePriority::HeaderFirst,
147            header_names: vec!["Authorization".to_string()],
148            cookie_names: vec![TOKEN_COOKIE_NAME.to_string(), "token".to_string()],
149        }
150    }
151}
152
153#[cfg(feature = "actix")]
154/// ## 日本語
155///
156/// actix-web のリクエストから token を抽出します。
157///
158/// `app_data(web::Data<TokenSourceConfig>)` があればその設定を使い、なければ
159/// `TokenSourceConfig::default()` にフォールバックします。
160///
161/// ## English
162///
163/// Extracts a token from an actix-web request.
164///
165/// The function reads configuration from `app_data(web::Data<TokenSourceConfig>)` if present;
166/// otherwise it falls back to `TokenSourceConfig::default()`.
167pub fn extract_token_from_request(req: &actix_web::HttpRequest) -> Option<String> {
168    use actix_web::web;
169
170    if let Some(cfg) = req.app_data::<web::Data<TokenSourceConfig>>() {
171        extract_token_from_request_with_config(req, cfg.as_ref())
172    } else {
173        let default_cfg = TokenSourceConfig::default();
174        extract_token_from_request_with_config(req, &default_cfg)
175    }
176}
177
178#[cfg(feature = "actix")]
179/// ## 日本語
180///
181/// 明示的な設定を使って actix-web のリクエストから token を抽出します。
182///
183/// 解析のルール:
184/// - header は `Bearer <token>` と生の `<token>` の両方に対応します。
185/// - cookie は cookie value をそのまま token として扱います。
186///
187/// ## English
188///
189/// Extracts a token from an actix-web request using an explicit config.
190///
191/// Token parsing behavior:
192/// - Header values support both `Bearer <token>` and raw `<token>` formats.
193/// - Cookie values use the raw cookie value as the token.
194pub fn extract_token_from_request_with_config(
195    req: &actix_web::HttpRequest,
196    cfg: &TokenSourceConfig,
197) -> Option<String> {
198    // 日本語: header 名を順に見て、最初に見つかった token を返す。
199    //        `Authorization: Bearer <token>` と `Authorization: <token>` の両方に対応する。
200    //        - 値が UTF-8 でない header は無視する(to_str() が失敗するため)
201    // English: Scan header names in order and return the first token found.
202    //          Supports both `Authorization: Bearer <token>` and raw `Authorization: <token>`.
203    //          - Non-UTF8 headers are ignored (to_str() fails)
204    let from_headers = || {
205        cfg.header_names.iter().find_map(|name| {
206            req.headers()
207                .get(name)
208                .and_then(|h| h.to_str().ok())
209                .map(|token_str| {
210                    token_str
211                        .strip_prefix("Bearer ")
212                        .unwrap_or(token_str)
213                        .to_string()
214                })
215        })
216    };
217
218    // 日本語: cookie 名を順に見て、最初に見つかった token を返す(cookie value をそのまま使う)。
219    // English: Scan cookie names in order and return the first token found (uses cookie value as-is).
220    let from_cookies = || {
221        cfg.cookie_names
222            .iter()
223            .find_map(|name| req.cookie(name).map(|cookie| cookie.value().to_string()))
224    };
225
226    // 日本語: 設定された優先順位に従って header/cookie を選ぶ。
227    //        両方に token がある場合でも「どちらを優先するか」をここで決める。
228    // English: Choose header vs cookie by configured priority.
229    //          This decides which source wins when both are present.
230    match cfg.priority {
231        TokenSourcePriority::HeaderFirst => from_headers().or_else(from_cookies),
232        TokenSourcePriority::CookieFirst => from_cookies().or_else(from_headers),
233    }
234}
235
236pub use crate::memory::RTokenManager;
237#[cfg(feature = "actix")]
238pub use crate::memory::RUser;
239pub use crate::models::RTokenError;
240#[cfg(all(feature = "redis", feature = "actix"))]
241pub use crate::redis::RRedisUser;
242#[cfg(feature = "redis")]
243pub use crate::redis::RTokenRedisManager;