r-token 1.2.0

A simple and efficient token generation library for Rust, ideal for API authentication and session management.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# r-token

README: 日本語(このページ) | [English]README.en.md

**r-token** は Rust の token 認証ヘルパーです。`actix-web``axum` の両方で、handler の引数に extractor(`RUser` / `RRedisUser`)を書くだけで認証済みコンテキストを取得できます。

この crate が提供するのは「認証(authentication)」のための primitives です:

- token の発行(login)
- token の検証(extractor / validate)
- token の失効(logout)
- TTL による自動失効(in-memory / Redis)
- 任意で roles を付与(RBAC)

バックエンドは 2 種類です:

- **インメモリ**: `RTokenManager`(期限は「Unix epoch ミリ秒」の絶対時刻で追跡)
- **Redis/Valkey(任意)**: `RTokenRedisManager`(期限は Redis TTL(秒)で強制)

## 目次

- [特長]#特長
- [対応マトリクス]#対応マトリクス
- [セキュリティ注意]#セキュリティ注意
- [インストール]#インストール
- [Feature flags]#feature-flags
- [まず何を選べばいい?(早見表)]#まず何を選べばいい早見表
- [認証の基本フロー]#認証の基本フロー
- [Token の取得元(Header / Cookie)]#token-の取得元header--cookie
- [Quickstart:actix-web(in-memory)]#quickstartactix-webin-memory
- [Quickstart:axum(in-memory)]#quickstartaxumin-memory
- [Redis/Valkey バックエンド]#redisvalkey-バックエンド
- [RBAC(roles)]#rbacroles
- [TTL 操作(renew / rotate / ttl_seconds など)]#ttl-操作renew--rotate--ttl_seconds-など
- [エラーとステータスコード]#エラーとステータスコード
- [例:このリポジトリのサンプルサーバー]#例このリポジトリのサンプルサーバー
- [テスト]#テスト
- [FAQ / トラブルシュート]#faq--トラブルシュート
- [ライセンス]#ライセンス

## 特長

- **Extractor-first**: `RUser` / `RRedisUser` を引数に書くだけでルートを保護
- **最小のボイラープレート**: header/cookie パース用の独自ミドルウェアが不要
- **TTL 対応**: インメモリ TTL と Redis TTL の両方をサポート
- **Token source 設定**: header/cookie 名と優先順位を `TokenSourceConfig` で制御
- **RBAC(任意)**: roles の付与と検証(`rbac` feature)
- **Valkey 対応**: Redis 互換プロトコルなら同様に利用可能(接続は `redis` crate)

## 対応マトリクス

| 目的 | in-memory | Redis/Valkey |
|---|---:|---:|
| actix-web extractor | `RUser` | `RRedisUser``redis-actix`|
| axum extractor | `RUser` | `RRedisUser``redis-axum`|
| TTL / 期限切れ | アプリ側(期限 ms を検証時に掃除) | Redis 側(TTL 秒で削除) |
| roles(RBAC) | `rbac` feature | `rbac` feature(value は JSON) |

## セキュリティ注意

- bearer-token 認証です。本番では必ず HTTPS を使ってください。
- token 文字列はアクセス権そのものです。ログ出力しない/クライアントに平文保存しないなど、パスワード同様に扱ってください。
- cookie で運ぶ場合は、基本的に `Secure` / `HttpOnly` / `SameSite` を検討してください(サンプルは簡潔さ優先で `HttpOnly` のみ)。
- Redis バックエンドは value として `user_id`(RBAC 有効時は JSON)を保存します。Redis の漏えいを前提にする場合は、token を hash 化して key として保存する方式を検討してください(現状この crate では未実装)。

## インストール

```toml
[dependencies]
r-token = "1.2.0"
```

MSRV(最小 Rust バージョン)は `Cargo.toml` の `rust-version` を参照してください。

## Feature flags

依存を任意化するため、機能は Cargo features で切り替えます:

- `actix`(デフォルト): actix-web 連携(`RUser` / `RRedisUser` の actix extractor)
- `axum`: axum 連携(`RUser` / `RRedisUser` の axum extractor、Tokio が必要)
- `redis`: Redis/Valkey バックエンド(Tokio が必要)
- `redis-actix`: 便利 feature(`redis` + `actix`- `redis-axum`: 便利 feature(`redis` + `axum`- `rbac`: roles を含む RBAC サポート(Serde が必要)

例:

```toml
[dependencies]
r-token = { version = "1.2.0", default-features = false, features = ["actix"] }
```

```toml
[dependencies]
r-token = { version = "1.2.0", default-features = false, features = ["axum"] }
```

```toml
[dependencies]
r-token = { version = "1.2.0", features = ["redis-actix"] }
```

```toml
[dependencies]
r-token = { version = "1.2.0", features = ["redis-axum", "rbac"] }
```

## まず何を選べばいい?(早見表)

- **actix-web + in-memory**: `r-token = "1.2.0"`(デフォルト `actix`- **axum + in-memory**: `default-features = false, features = ["axum"]`
- **actix-web + Redis/Valkey**: `features = ["redis-actix"]`
- **axum + Redis/Valkey**: `features = ["redis-axum"]`
- **roles も必要**: 上記に `rbac` を追加

## 認証の基本フロー

一般的な API の流れは次の通りです:

1. `/login`(公開)で `login(..)` を呼び、token を返す(必要なら cookie もセット)
2. クライアントは token を以後のリクエストに付与する
   - `Authorization: <token>`
   - `Authorization: Bearer <token>`
   - または cookie(既定は `r_token`3. 保護したい handler に `RUser` / `RRedisUser` を引数として書く
4. extractor が成功すれば、handler 内で `user.id`(RBAC 有効なら `user.roles`)が使える

## Token の取得元(Header / Cookie)

既定では `Authorization` header と cookie から token を探します。

対応する header 形式:

```text
Authorization: <token>
Authorization: Bearer <token>
```

cookie は `r_token`(既定)を探索します。探索ルールは `TokenSourceConfig` で変更できます:

- `header_names`: 順に探す header 名(例:`Authorization``X-Api-Token`- `cookie_names`: 順に探す cookie 名(例:`r_token`- `priority`: HeaderFirst / CookieFirst

actix-web では `app_data(web::Data<TokenSourceConfig>)`、axum では `Extension(TokenSourceConfig)` で注入します。

## Quickstart:actix-web(in-memory)

### 1) 依存

```toml
[dependencies]
r-token = "1.2.0"
```

### 2) ルート(login / profile / logout)

保護したい handler に `RUser` を引数として追加します(手動パース不要)。

```rust
use actix_web::{get, post, web, HttpResponse, Responder};
use r_token::{RTokenError, RTokenManager, RUser};

#[post("/login")]
async fn login(
    manager: web::Data<RTokenManager>,
    body: String,
) -> Result<impl Responder, RTokenError> {
    let user_id = body.trim();
    let token = manager.login(user_id, 3600)?;
    Ok(HttpResponse::Ok().body(token))
}

#[get("/profile")]
async fn profile(user: RUser) -> impl Responder {
    format!("Profile: {}", user.id)
}

#[post("/logout")]
async fn logout(
    manager: web::Data<RTokenManager>,
    user: RUser,
) -> Result<impl Responder, RTokenError> {
    manager.logout(&user.token)?;
    Ok(HttpResponse::Ok().body("Logged out"))
}
```

### 3) アプリ state(必須)

```rust
use actix_web::{web, App, HttpServer};
use r_token::RTokenManager;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let manager = RTokenManager::new();
    HttpServer::new(move || App::new().app_data(web::Data::new(manager.clone())))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}
```

### 4) 試す(curl)

```bash
token=$(curl -s -X POST http://127.0.0.1:8080/login -d "alice")
curl -s -H "Authorization: $token" http://127.0.0.1:8080/profile
curl -s -X POST -H "Authorization: $token" http://127.0.0.1:8080/logout
```

## Quickstart:axum(in-memory)

### 1) 依存

```toml
[dependencies]
r-token = { version = "1.2.0", default-features = false, features = ["axum"] }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread"] }
```

### 2) ルート(login / profile / logout)

`Extension` で manager を注入します。`RUser` は handler 引数としてそのまま使えます。

```rust
use axum::{
    Router,
    extract::Extension,
    http::{HeaderValue, StatusCode, header},
    response::{IntoResponse, Response},
    routing::{get, post},
};
use r_token::{RTokenError, RTokenManager, RUser, TOKEN_COOKIE_NAME};

async fn login(Extension(manager): Extension<RTokenManager>, body: String) -> Result<Response, Response> {
    let user_id = body.trim();
    if user_id.is_empty() {
        return Err((StatusCode::BAD_REQUEST, "Empty user id").into_response());
    }

    let token = manager.login(user_id, 3600).map_err(|e: RTokenError| e.into_response())?;

    let mut resp = token.clone().into_response();
    let cookie = format!("{}={}; Path=/; HttpOnly", TOKEN_COOKIE_NAME, token);
    let cookie = HeaderValue::from_str(&cookie)
        .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Invalid cookie").into_response())?;
    resp.headers_mut().insert(header::SET_COOKIE, cookie);
    Ok(resp)
}

async fn profile(user: RUser) -> impl IntoResponse {
    format!("Profile: {}", user.id)
}

async fn logout(Extension(manager): Extension<RTokenManager>, user: RUser) -> Result<&'static str, RTokenError> {
    manager.logout(&user.token)?;
    Ok("Logged out")
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
    let manager = RTokenManager::new();
    let app = Router::new()
        .route("/login", post(login))
        .route("/profile", get(profile))
        .route("/logout", post(logout))
        .layer(Extension(manager));

    let listener = tokio::net::TcpListener::bind("127.0.0.1:8082").await?;
    axum::serve(listener, app).await?;
    Ok(())
}
```

### 3) 試す(curl)

```bash
token=$(curl -s -X POST http://127.0.0.1:8082/login -d "alice")
curl -s -H "Authorization: $token" http://127.0.0.1:8082/profile
curl -s -X POST -H "Authorization: $token" http://127.0.0.1:8082/logout
```

## Redis/Valkey バックエンド

`RTokenRedisManager` は非同期 API です(Tokio runtime が必要)。actix-web / axum のどちらでも、`RRedisUser` extractor が利用できます(`redis` + `actix` または `axum`)。

### 接続

```rust
use r_token::RTokenRedisManager;

let redis_url = "redis://127.0.0.1/";
let prefix = "r_token:token:";
let manager = RTokenRedisManager::connect(redis_url, prefix).await?;
```

`prefix` は環境ごと・アプリごとの分離に使います。末尾の `:` は自動で補われます。

### actix-web で使う

- manager は `web::Data<RTokenRedisManager>` として注入します
- 保護ルートは `RRedisUser` を引数に持ちます

サンプル実装は [redis_main.rs](file:///Volumes/P600/r-token/src/bin/redis_main.rs) を参照してください。

### axum で使う

- manager は `Extension(RTokenRedisManager)` として注入します
- 保護ルートは `RRedisUser` を引数に持ちます

サンプル実装は [redis_axum_main.rs](file:///Volumes/P600/r-token/src/bin/redis_axum_main.rs) を参照してください。

## RBAC(roles)

`rbac` feature を有効にすると、token に roles を紐づけられます。

利用できる主な API:

- in-memory:
  - `RTokenManager::login_with_roles(user_id, ttl, roles)`
  - `RTokenManager::set_roles(token, roles)`(冪等)
  - `RTokenManager::get_roles(token)`
  - `RUser.roles` / `RUser::has_role(..)`
- Redis:
  - `RTokenRedisManager::login_with_roles(user_id, ttl, roles)`
  - `RTokenRedisManager::set_roles(token, roles)`(冪等、TTL を保持)
  - `RTokenRedisManager::get_roles(token)`
  - `RTokenRedisManager::validate_with_roles(token)`
  - `RRedisUser.roles`

### 典型的な「認可」パターン

r-token は「認証」までを担当します。認可(特定の role が必要、など)はアプリ側で実装します。

actix-web:

```rust
use actix_web::{get, HttpResponse};
use r_token::RUser;

#[get("/admin")]
async fn admin(user: RUser) -> HttpResponse {
    if !user.has_role("admin") {
        return HttpResponse::Forbidden().body("forbidden");
    }
    HttpResponse::Ok().body("ok")
}
```

## TTL 操作(renew / rotate / ttl_seconds など)

### in-memory(RTokenManager)

- `expires_at(token) -> Option<u64>`: 保存されている期限(ms)。期限切れ判定はしない
- `ttl_seconds(token) -> Option<i64>`: 残り TTL(秒)。期限切れは `Some(0)`
- `renew(token, ttl) -> bool`: 期限を延長(期限切れは削除して `false`- `rotate(token, ttl) -> Option<String>`: 新 token を発行し old token を失効
- `prune_expired() -> usize`: 期限切れ token を掃除(件数を返す)

### Redis/Valkey(RTokenRedisManager)

- `ttl_seconds(token) -> Option<i64>`: Redis TTL の意味をそのまま返す
  - `None`: key が存在しない
  - `Some(-1)`: key は存在するが期限がない
  - `Some(n)`(n >= 0): 残り TTL(秒)
- `renew(token, ttl) -> bool`: `EXPIRE` による延長
- `rotate(token, ttl) -> Option<String>`: 新 token を発行して old key を削除(簡潔さ優先のため原子的ではない)

## エラーとステータスコード

### `RTokenError`

`RTokenManager` が返すエラーは現在 `MutexPoisoned` のみです。

- actix-web: `actix_web::ResponseError` を実装しているため handler からそのまま返せます
- axum: `IntoResponse` を実装しており、既定では 500 を返します(詳細は `Display` の文字列)

### extractor の 401 / 500

共通の失敗パターン:

- **401 Unauthorized**: token が無い / 無効 / 期限切れ
- **500 Internal Server Error**: manager が state に注入されていない(設定忘れ)、または内部の mutex が poisoned

## 例:このリポジトリのサンプルサーバー

in-memory(actix-web、8080):

```bash
cargo run --bin r-token
```

Redis/Valkey(actix-web、8081):

```bash
REDIS_URL=redis://127.0.0.1/ R_TOKEN_PREFIX=r_token:token: \
  cargo run --bin r-token-redis --features redis-actix
```

in-memory(axum、8082):

```bash
cargo run --bin r-token-axum --features axum
```

Redis/Valkey(axum、8083):

```bash
REDIS_URL=redis://127.0.0.1/ R_TOKEN_PREFIX=r_token:token: \
  cargo run --bin r-token-redis-axum --features redis-axum
```

## テスト

基本:

```bash
cargo test
```

全 feature:

```bash
cargo test --all-features
```

Redis テストについて:

- `REDIS_URL` が指定されていない場合、テストは `redis-server` をローカルで起動しようとします。
- `redis-server` が利用できない環境では、`REDIS_URL` を設定して既存の Redis/Valkey を使ってください。

## FAQ / トラブルシュート

### 401 になる

- token を送っていない(`Authorization` / cookie のどちらも無い)
- `Authorization` の形式が違う(`Bearer ` を付ける/付けないはどちらも可)
- token が期限切れ(in-memory では検証時に削除されます。Redis では TTL で消えます)
- `TokenSourceConfig` を変更していて header/cookie 名が一致していない

### 500 になる(Token manager not found / Redis error)

- actix-web: `web::Data<RTokenManager>` / `web::Data<RTokenRedisManager>``app_data` に入れ忘れている
- axum: `Extension(RTokenManager)` / `Extension(RTokenRedisManager)``.layer(..)` していない
- Redis: 接続先が落ちている、URL が間違っている

## ライセンス

MIT