r-token 1.0.1

A simple and efficient token generation library for Rust, ideal for API authentication and session management.
Documentation
# 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