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 / axum 向けの軽量なインメモリ token 認証ヘルパーです。
14//!
15//! このライブラリは主に次の 2 つを提供します:
16//! - [`RTokenManager`]: token(UUID v4)の発行/失効と、インメモリストアの管理
17//! - [`RUser`]: `Authorization` を自動検証する extractor(actix-web / axum)
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 and axum.
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 extractor that validates `Authorization` automatically (actix-web / axum).
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 the framework returns an error.
45
46#[cfg(feature = "axum")]
47mod axum_support;
48mod memory;
49mod models;
50#[cfg(feature = "redis")]
51mod redis;
52
53/// ## 日本語
54///
55/// token 送受信に使うデフォルトの Cookie 名です。
56///
57/// この名前は次で使用されます:
58/// - 例のサーバーが `/login` で Cookie をセットするとき
59/// - actix extractor が Cookie から token を読むとき
60///
61/// ## English
62///
63/// Default cookie name used for token transport.
64///
65/// This name is used by:
66/// - the example servers when setting cookies on `/login`
67/// - the actix extractors when reading the token from cookies
68pub const TOKEN_COOKIE_NAME: &str = "r_token";
69
70#[cfg(any(feature = "actix", feature = "axum"))]
71#[derive(Clone, Debug)]
72/// ## 日本語
73///
74/// 複数の token 供給元がある場合に、どちらを優先するかの設定です。
75///
76/// ## English
77///
78/// Priority for selecting which token source to use when multiple are present.
79pub enum TokenSourcePriority {
80 /// ## 日本語
81 ///
82 /// Header(例:`Authorization`)を Cookie より優先します。
83 ///
84 /// ## English
85 ///
86 /// Prefer headers (e.g. `Authorization`) over cookies.
87 HeaderFirst,
88 /// ## 日本語
89 ///
90 /// Cookie を header より優先します。
91 ///
92 /// ## English
93 ///
94 /// Prefer cookies over headers.
95 CookieFirst,
96}
97
98#[cfg(any(feature = "actix", feature = "axum"))]
99#[derive(Clone, Debug)]
100/// ## 日本語
101///
102/// actix extractor の token 取得元を設定します。
103///
104/// axum でも同じ設定を使えます(`Extension<TokenSourceConfig>` として注入)。
105///
106/// `app_data(web::Data<TokenSourceConfig>)` として登録すると、次をカスタマイズできます:
107/// - どの header 名を順に探索するか
108/// - どの cookie 名を順に探索するか
109/// - header/cookie の優先順位
110///
111/// ## English
112///
113/// Token source configuration for actix extractors.
114///
115/// The same config can also be used by axum (inject via `Extension<TokenSourceConfig>`).
116///
117/// You can register this as `app_data(web::Data<TokenSourceConfig>)` to customize:
118/// - which header names are scanned for a token
119/// - which cookie names are scanned for a token
120/// - the priority order between header/cookie
121pub struct TokenSourceConfig {
122 /// ## 日本語
123 ///
124 /// token 取得元の優先順位。
125 ///
126 /// ## English
127 ///
128 /// Priority of token sources.
129 pub priority: TokenSourcePriority,
130 /// ## 日本語
131 ///
132 /// 順にチェックする header 名の一覧。
133 ///
134 /// ## English
135 ///
136 /// Header names that will be checked in order.
137 pub header_names: Vec<String>,
138 /// ## 日本語
139 ///
140 /// 順にチェックする cookie 名の一覧。
141 ///
142 /// ## English
143 ///
144 /// Cookie names that will be checked in order.
145 pub cookie_names: Vec<String>,
146}
147
148#[cfg(any(feature = "actix", feature = "axum"))]
149impl Default for TokenSourceConfig {
150 fn default() -> Self {
151 Self {
152 priority: TokenSourcePriority::HeaderFirst,
153 header_names: vec!["Authorization".to_string()],
154 cookie_names: vec![TOKEN_COOKIE_NAME.to_string()],
155 }
156 }
157}
158
159#[cfg(any(feature = "actix", feature = "axum"))]
160pub(crate) fn extract_token_with_config(
161 cfg: &TokenSourceConfig,
162 mut header_value: impl FnMut(&str) -> Option<String>,
163 mut cookie_value: impl FnMut(&str) -> Option<String>,
164) -> Option<String> {
165 // 日本語: header/cookie のどちらから token を取るかは設定の優先順位で決める。
166 // English: Choose header vs cookie based on configured priority.
167 let mut from_headers = || {
168 // 日本語: 複数の header 名を順に試し、最初に見つかった値を token として扱う。
169 // "Bearer <token>" と "<token>" の両方を受け付ける。
170 // English: Try header names in order and take the first match.
171 // Accept both "Bearer <token>" and raw "<token>".
172 cfg.header_names.iter().find_map(|name| {
173 header_value(name).map(|token_str| {
174 token_str
175 .strip_prefix("Bearer ")
176 .unwrap_or(token_str.as_str())
177 .to_string()
178 })
179 })
180 };
181
182 // 日本語: 複数の cookie 名を順に試し、最初に見つかった値を token として扱う。
183 // English: Try cookie names in order and take the first match.
184 let mut from_cookies = || cfg.cookie_names.iter().find_map(|name| cookie_value(name));
185
186 match cfg.priority {
187 // 日本語: HeaderFirst は header を優先し、無ければ cookie を見る。
188 // English: HeaderFirst prefers headers, falling back to cookies.
189 TokenSourcePriority::HeaderFirst => from_headers().or_else(from_cookies),
190 // 日本語: CookieFirst は cookie を優先し、無ければ header を見る。
191 // English: CookieFirst prefers cookies, falling back to headers.
192 TokenSourcePriority::CookieFirst => from_cookies().or_else(from_headers),
193 }
194}
195
196#[cfg(feature = "actix")]
197/// ## 日本語
198///
199/// actix-web のリクエストから token を抽出します。
200///
201/// `app_data(web::Data<TokenSourceConfig>)` があればその設定を使い、なければ
202/// `TokenSourceConfig::default()` にフォールバックします。
203///
204/// ## English
205///
206/// Extracts a token from an actix-web request.
207///
208/// The function reads configuration from `app_data(web::Data<TokenSourceConfig>)` if present;
209/// otherwise it falls back to `TokenSourceConfig::default()`.
210pub fn extract_token_from_request(req: &actix_web::HttpRequest) -> Option<String> {
211 use actix_web::web;
212
213 if let Some(cfg) = req.app_data::<web::Data<TokenSourceConfig>>() {
214 extract_token_from_request_with_config(req, cfg.as_ref())
215 } else {
216 let default_cfg = TokenSourceConfig::default();
217 extract_token_from_request_with_config(req, &default_cfg)
218 }
219}
220
221#[cfg(feature = "actix")]
222/// ## 日本語
223///
224/// 明示的な設定を使って actix-web のリクエストから token を抽出します。
225///
226/// 解析のルール:
227/// - header は `Bearer <token>` と生の `<token>` の両方に対応します。
228/// - cookie は cookie value をそのまま token として扱います。
229///
230/// ## English
231///
232/// Extracts a token from an actix-web request using an explicit config.
233///
234/// Token parsing behavior:
235/// - Header values support both `Bearer <token>` and raw `<token>` formats.
236/// - Cookie values use the raw cookie value as the token.
237pub fn extract_token_from_request_with_config(
238 req: &actix_web::HttpRequest,
239 cfg: &TokenSourceConfig,
240) -> Option<String> {
241 extract_token_with_config(
242 cfg,
243 |name| {
244 req.headers()
245 .get(name)
246 .and_then(|h| h.to_str().ok())
247 .map(|s| s.to_string())
248 },
249 |name| req.cookie(name).map(|cookie| cookie.value().to_string()),
250 )
251}
252
253pub use crate::memory::RTokenManager;
254#[cfg(any(feature = "actix", feature = "axum"))]
255pub use crate::memory::RUser;
256pub use crate::models::RTokenError;
257#[cfg(all(feature = "redis", any(feature = "actix", feature = "axum")))]
258pub use crate::redis::RRedisUser;
259#[cfg(feature = "redis")]
260pub use crate::redis::RTokenRedisManager;