# r-token
**r-token** is a small token authentication helper for Rust and `actix-web`.
It provides two token managers:
- **In-memory**: `RTokenManager` stores tokens in memory with an expiration timestamp.
- **Redis/Valkey** (optional): `RTokenRedisManager` stores tokens in Redis with TTL.
For `actix-web`, r-token follows a “parameter-as-authentication” style: add `RUser` to handler parameters, and the request is authenticated automatically via the Actix extractor mechanism.
## Features
- **Zero boilerplate**: no custom middleware required for basic header auth.
- **Extractor-first**: declaring `RUser` protects the route.
- **Thread-safe, shared state**: `RTokenManager` is `Clone` and shares an in-memory store.
- **TTL support**:
- In-memory: tokens expire based on a per-login TTL (seconds).
- Redis/Valkey: expiration is enforced by Redis TTL (seconds).
- **Redis/Valkey backend (optional)**: `RTokenRedisManager` stores `user_id` by token key.
## Security notes
- This library implements bearer-token authentication. Always use HTTPS in production.
- Token strings grant access. Treat them like passwords: do not log them, do not store them in plaintext client storage without careful threat modeling.
- The Redis backend stores `user_id` as the Redis value. If you need stronger protection against Redis data disclosure, consider storing a hashed token (not currently implemented by this crate).
## Status
This project is in active development. Review the source code and tests before adopting it in security-sensitive environments.
We have released a stable version, but the API is not yet frozen. We will maintain backward compatibility and will not introduce breaking changes.
## Installation
Add r-token to your `Cargo.toml`:
```toml
[dependencies]
r-token = "1.0.0"
```
## Feature flags
r-token uses Cargo features to keep dependencies optional:
- `actix` (default): enables the `RUser` extractor and actix-web integration.
- `redis`: enables Redis/Valkey support via the `redis` crate.
- `redis-actix`: convenience feature = `redis` + `actix`.
- `rbac`: enables role-based access control (RBAC) support.
Examples:
```toml
[dependencies]
r-token = { version = "1.0.0", default-features = false }
```
```toml
[dependencies]
r-token = { version = "1.0.0", features = ["redis-actix"] }
```
```toml
[dependencies]
r-token = { version = "1.0.0", features = ["rbac"] }
```
```toml
[dependencies]
r-token = { version = "1.0.0", features = ["redis-actix", "rbac"] }
```
## Authorization header
The `RUser` extractor (and the Redis example server) reads the token from `Authorization` and supports:
```text
Authorization: <token>
Authorization: Bearer <token>
```
## API overview
Core types:
- `RTokenManager` (always available): issues and revokes tokens in memory.
- `RTokenError` (always available): error type used by in-memory manager.
Actix integration (requires `actix`, enabled by default):
- `RUser`: `actix_web::FromRequest` extractor that validates `Authorization`.
Redis backend (requires `redis`):
- `RTokenRedisManager`: issues, validates, and revokes tokens backed by Redis/Valkey.
RBAC support (requires `rbac`):
- `RTokenManager::login_with_roles()`: issues a token with associated roles.
- `RTokenManager::set_roles()`: updates roles for an existing token.
- `RTokenManager::get_roles()`: retrieves roles for a token.
- `RUser.roles`: vector of roles associated with the authenticated user.
- `RUser::has_role()`: checks if the user has a specific role.
- `RTokenRedisManager::login_with_roles()`: issues a token with roles in Redis.
- `RTokenRedisManager::set_roles()`: updates roles for a token in Redis.
- `RTokenRedisManager::get_roles()`: retrieves roles for a token in Redis.
- `RTokenRedisManager::validate()`: returns both `user_id` and `roles` when RBAC is enabled.
## In-memory usage (actix-web)
### 1. Add endpoints
No manual header parsing is needed. Inject `RUser` into protected handlers.
```rust
use actix_web::{get, post, web, HttpResponse, Responder};
use r_token::{RTokenManager, RUser, RTokenError};
#[post("/login")]
async fn login(
manager: web::Data<RTokenManager>,
body: String,
) -> Result<impl Responder, RTokenError> {
let token = manager.login(&body, 3600)?;
Ok(HttpResponse::Ok().body(token))
}
#[get("/info")]
async fn info(user: RUser) -> impl Responder {
format!("info: {}", 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 successfully"))
}
```
### 2. Register and Run
Initialize `RTokenManager` and register it with your Actix application.
```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()))
.service(login)
.service(info)
.service(logout)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
```
## RBAC usage (role-based access control)
When the `rbac` feature is enabled, you can assign roles to tokens and perform role-based authorization.
### In-memory RBAC
```rust
use r_token::{RTokenManager, RUser, RTokenError};
use actix_web::{get, post, web, HttpResponse, Responder};
#[post("/login")]
async fn login(
manager: web::Data<RTokenManager>,
body: String,
) -> Result<impl Responder, RTokenError> {
// Parse user_id and roles from request body
let parts: Vec<&str> = body.split(':').collect();
let user_id = parts[0];
let roles: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
let token = manager.login_with_roles(user_id, 3600, roles)?;
Ok(HttpResponse::Ok().body(token))
}
#[get("/admin")]
async fn admin_only(user: RUser) -> impl Responder {
if user.has_role("admin") {
HttpResponse::Ok().body(format!("Welcome, admin {}", user.id))
} else {
HttpResponse::Forbidden().body("Access denied: admin role required")
}
}
#[post("/promote")]
async fn promote(
manager: web::Data<RTokenManager>,
user: RUser,
) -> Result<impl Responder, RTokenError> {
// Only admins can promote users
if !user.has_role("admin") {
return Ok(HttpResponse::Forbidden().body("Access denied"));
}
// Add 'moderator' role to the current user
manager.set_roles(&user.token, vec!["admin".to_string(), "moderator".to_string()])?;
Ok(HttpResponse::Ok().body("Promoted to moderator"))
}
#[get("/roles")]
async fn get_user_roles(user: RUser) -> impl Responder {
HttpResponse::Ok().json(&user.roles)
}
```
### Redis RBAC
```rust
use r_token::RTokenRedisManager;
#[tokio::main]
async fn main() -> Result<(), redis::RedisError> {
let manager = RTokenRedisManager::connect("redis://127.0.0.1/", "r_token:token:")
.await?;
// Create token with roles
let roles = vec!["admin".to_string(), "editor".to_string()];
let token = manager.login_with_roles("alice", 3600, roles).await?;
// Validate and get user info with roles
let user_info = manager.validate_with_roles(&token).await?;
if let Some((user_id, retrieved_roles)) = user_info {
println!("User: {}, Roles: {:?}", user_id, retrieved_roles);
}
// Update roles
manager.set_roles(&token, vec!["admin".to_string()]).await?;
// Get roles only
let roles = manager.get_roles(&token).await?;
println!("Roles: {:?}", roles);
manager.logout(&token).await?;
Ok(())
}
```
## Behavioral details
In-memory manager:
- `RTokenManager::login(user_id, ttl_seconds)` returns a UUID v4 token string.
- `RTokenManager::renew(token, ttl_seconds)` extends an existing token lifetime.
- `RTokenManager::rotate(token, ttl_seconds)` issues a new token and revokes the old one.
- `RTokenManager::ttl_seconds(token)` returns remaining TTL.
- Expiration is tracked by storing an absolute expiration timestamp (milliseconds since Unix epoch).
- Expired tokens are removed when you call `validate()` (and will otherwise remain in memory).
- You can proactively remove expired tokens via `RTokenManager::prune_expired()`.
- `RTokenManager::logout(token)` is idempotent: revoking a non-existent token is treated as success.
Actix extractor:
- On success, `RUser` provides `id` and the raw `token`.
- When RBAC is enabled, `RUser` also provides `roles` (a vector of role strings).
- `RUser::has_role(role)` checks if the user has a specific role.
- Failure modes:
- `401 Unauthorized`: missing token, invalid token, or expired token.
- `500 Internal Server Error`: token manager missing from `app_data`, or internal mutex poisoned.
Redis manager:
- `RTokenRedisManager::login(user_id, ttl_seconds)` stores `prefix + token` as the key and `user_id` as the value, with Redis TTL set to `ttl_seconds`.
- `RTokenRedisManager::renew(token, ttl_seconds)` updates the Redis TTL for the token key.
- `RTokenRedisManager::rotate(token, ttl_seconds)` issues a new token and deletes the old key.
- `RTokenRedisManager::ttl_seconds(token)` returns Redis TTL semantics for the token key.
- `validate(token)` returns `Ok(None)` when the key is absent (revoked or expired).
- When RBAC is enabled, `validate(token)` returns `Ok(Some(user_id))` (roles require `validate_with_roles(token)`).
- When RBAC is enabled, `validate_with_roles(token)` returns `Ok(Some((user_id, roles)))`.
- `logout(token)` deletes the key and is idempotent.
RBAC behavior:
- Tokens can be created with roles via `login_with_roles()`.
- Roles can be updated on existing tokens via `set_roles()`.
- Roles can be retrieved via `get_roles()`.
- When RBAC is enabled, `RUser.roles` is available (empty vector if no roles were assigned).
- `RUser::has_role()` performs a case-sensitive string comparison.
## Redis/Valkey usage
If you want token persistence and Redis-managed TTL expiration, enable `redis` (or `redis-actix`) and use `RTokenRedisManager`.
You also need a Tokio runtime in your application (do not rely on transitive dependencies):
```toml
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
```rust
use r_token::RTokenRedisManager;
#[tokio::main]
async fn main() -> Result<(), redis::RedisError> {
let manager = RTokenRedisManager::connect("redis://127.0.0.1/", "r_token:token:")
.await?;
let token = manager.login("alice", 3600).await?;
let user_id = manager.validate(&token).await?;
assert_eq!(user_id.as_deref(), Some("alice"));
manager.logout(&token).await?;
Ok(())
}
```
## Usage examples (curl)
### Login
```bash
curl -X POST http://127.0.0.1:8080/login -d "alice"
# Response: 550e8400-e29b-41d4-a716-446655440000
```
### Access Protected Resource
```bash
# Without Token -> 401 Unauthorized
curl http://127.0.0.1:8080/info
# With Token -> 200 OK
curl -H "Authorization: <token>" http://127.0.0.1:8080/info
```
## Example servers in this repo
### In-memory (actix-web)
```bash
cargo run --bin r-token
```
### Redis/Valkey (actix-web)
Environment variables:
- `REDIS_URL` (default: `redis://127.0.0.1/`)
- `R_TOKEN_PREFIX` (default: `r_token:token:`)
```bash
REDIS_URL=redis://127.0.0.1/ cargo run --bin r-token-redis --features redis-actix
```
## Roadmap
- [x] In-memory token management + extractor
- [x] Token expiration (TTL)
- [x] Redis/Valkey backend token storage (optional)
- [x] Role-based access control (RBAC)
- [x] Cookie support
- [x] In-memory token validation API (non-actix)
- [x] Redis actix-web extractor (parameter-as-authentication)
- [x] Configurable token sources (header/cookie name, priority)
## License
MIT