Skip to content

Authentication

Oxide ships Sanctum-style token auth in the oxide-auth crate. A single personal_access_tokens table supports multiple user entities via a polymorphic tokenable_type, so you can run User and Customer (or Admin, or anything else) alongside each other.

Install the Sanctum stubs

From your project root:

Terminal window
oxide vendor:publish sanctum
oxide migrate

That publishes the create_personal_access_tokens_table migration into src/database/migrations/ and drops config/sanctum.toml for you to edit.

Make an entity authenticatable

Two traits: Authenticatable on the Entity (for the morph type) and HasApiTokens on the Model (for token creation + listing).

// in src/app/domain/user/models/user.rs
use oxide_db::sea_orm::entity::prelude::*;
use oxide_db::timestamps_behavior;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "users")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
#[sea_orm(unique)]
pub email: String,
pub password: String,
pub created_at: Option<ChronoDateTimeUtc>,
pub updated_at: Option<ChronoDateTimeUtc>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
timestamps_behavior!();
impl oxide_auth::Authenticatable for Entity {
const MORPH_TYPE: &'static str = "User";
}
impl oxide_auth::HasApiTokens for Model {
type Entity = Entity;
fn model_id(&self) -> i64 {
self.id
}
}

Register + login flow

use oxide_auth::{Hash, HasApiTokens};
use oxide_db::sea_orm::{ActiveModelTrait, Set};
use oxide_db::conn;
use crate::app::domain::user::models::{User, UserActiveModel};
pub async fn register(form: RegisterRequest) -> Response {
let hashed = Hash::make(form.password.expect("validated"))
.await
.expect("hash ok");
let saved = UserActiveModel {
name: Set(form.name.expect("validated")),
email: Set(form.email.expect("validated")),
password: Set(hashed),
..Default::default()
}
.insert(conn())
.await
.expect("insert ok");
// Issue a full-access token.
let nt = saved
.create_token("auth_token", vec!["*".into()], None)
.await
.expect("token ok");
Response::json(&serde_json::json!({
"user": { "id": saved.id, "email": saved.email },
"token": nt.plain_text_token,
}))
.unwrap()
}

Hash::make and Hash::check are bcrypt wrappers that read BCRYPT_ROUNDS (default 12, matching Laravel).

Protecting routes

use oxide_auth::auth_middleware;
use crate::app::domain::user::models::User;
app.middleware([auth_middleware::<User>()])
.get("/me", UserController::me)
.post("/auth/logout", AuthController::logout);

The middleware verifies the Bearer token, attaches Authed<User> to the request extensions, and responds 401 on failure.

Reading the authed user

use oxide_auth::Authed;
use crate::app::domain::user::models::User;
pub async fn me(req: Request) -> Response {
let authed = req
.extensions()
.get::<Authed<User>>()
.expect("auth_middleware must run first");
Response::json(&serde_json::json!({
"id": authed.user.id,
"email": authed.user.email,
"token_name": authed.token.name,
}))
.unwrap()
}

Multi-tenant

Both User and Customer can share the personal_access_tokens table. Give each its own MORPH_TYPE constant:

impl oxide_auth::Authenticatable for customer::Entity {
const MORPH_TYPE: &'static str = "Customer";
}

A User token presented at /customer/me returns 401 because auth_middleware::<Customer>() checks the morph type.