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:
oxide vendor:publish sanctumoxide migrateThat 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.rsuse 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.