Skip to content

Tokens and Abilities

Tokens carry a list of abilities — arbitrary strings such as todos:read or admin:* — that gate what the token may do. This matches Laravel Sanctum’s “abilities” concept; the term is used in preference to scope to avoid overloading an already-loaded word.

Issuing a scoped token

Pass an abilities list when creating the token:

// Full access
let nt = user.create_token("full", vec!["*".into()], None).await?;
// Read-only on todos
let nt = user.create_token("todos-read", vec!["todos:read".into()], None).await?;
// Multiple
let nt = user.create_token("editor", vec![
"todos:read".into(),
"todos:write".into(),
], None).await?;

The wildcard "*" satisfies every ability check.

Token format

Plain token returned to the client:

{token_id}|{prefix}{40_alphanumerics}{crc32b}
  • token_id — the row’s primary key
  • prefixconfig/sanctum.toml’s token_prefix (empty by default)
  • 40 random alphanumerics — the entropy
  • crc32b — 8-char checksum for scanner recognition

Only the SHA-256 of everything after the pipe is stored in the DB. Verification uses constant-time comparison to defeat timing attacks.

Gating with middleware

use oxide_auth::{auth_middleware, abilities, ability};
use crate::app::domain::user::models::User;
// Require EVERY listed ability:
app.middleware([auth_middleware::<User>(), abilities(["todos:write"])])
.post("/todos", TodoController::store);
// Require ANY of the listed abilities:
app.middleware([auth_middleware::<User>(), ability(["todos:read", "todos:write"])])
.get("/todos", TodoController::index);

auth_middleware::<User>() must come first — it populates the token into the request extensions so abilities and ability can read it.

Inline ability check

If you need finer control inside a handler:

use oxide_auth::Authed;
pub async fn delete(req: Request) -> Response {
let authed = req.extensions().get::<Authed<User>>().expect("authed");
if !authed.token.can("todos:delete") {
return Response::forbidden_with("Missing ability: todos:delete");
}
// ...
Response::text("ok")
}

token.can(ability) returns true if the token’s abilities array contains "*" or the exact ability.

Expiration

Set per-call:

use chrono::{Utc, Duration};
let nt = user.create_token(
"temp",
vec!["*".into()],
Some(Utc::now() + Duration::hours(1)),
).await?;

Or globally via config/sanctum.toml:

expiration = "${SANCTUM_EXPIRATION:-60}" # minutes; 0 = never expires

A token whose expires_at has passed returns 401 on verification.

Revoking

use oxide_auth::Auth;
// Revoke by id (e.g. logout of current device):
let Some(id) = Auth::token_id_from(&req) else {
return Response::unauthorized();
};
Auth::revoke(id).await?;

To revoke every token belonging to a user, filter by tokenable_id and delete:

use oxide_auth::{PersonalAccessToken, PersonalAccessTokenColumn};
use oxide_db::conn;
use oxide_db::sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
PersonalAccessToken::delete_many()
.filter(PersonalAccessTokenColumn::TokenableType.eq("User"))
.filter(PersonalAccessTokenColumn::TokenableId.eq(user.id))
.exec(conn())
.await?;