//! Caller authentication for MCP tools: [`AuthUser`].
//!
//! This lives in `base` rather than in the `auth` module because every module's
//! tools check it; keeping it here means modules never depend on each other
//! just to check who is calling.
//!
//! The bearer token travels in the `Authorization` header of each MCP HTTP
//! request. Tools receive those headers through `Extension<Parts>` and pass
//! them to [`AuthUser::authenticate`] (usually via `McpServer::authenticate`).
use axum::http::HeaderMap;
use axum::http::header::AUTHORIZATION;
use super::error::{AppError, AppResult};
use super::state::AppState;
/// Role with full access, granted to the first registered user.
pub const ADMIN_ROLE: &str = "admin";
/// Role granted to every other newly registered user.
pub const USER_ROLE: &str = "user";
/// The user calling a tool, authenticated by `Authorization: Bearer <token>`.
#[derive(Debug, Clone)]
pub struct AuthUser {
pub id: i64,
pub name: String,
pub email: String,
pub roles: Vec<String>,
token_hash: String,
}
impl AuthUser {
/// Resolves the caller from a valid, unexpired bearer token in `headers`.
pub async fn authenticate(state: &AppState, headers: &HeaderMap) -> AppResult<Self> {
let token = bearer_token(headers).ok_or(AppError::Unauthorized)?;
let token_hash = slave_framework::auth::hash_token(token);
let conn = state.database.connect().await?;
let mut rows = conn
.query(
"SELECT u.id, u.name, u.email FROM auth_tokens t
JOIN users u ON u.id = t.user_id
WHERE t.token_hash = ?1 AND t.expires_at > CURRENT_TIMESTAMP",
(token_hash.as_str(),),
)
.await?;
let Some(row) = rows.next().await? else {
return Err(AppError::Unauthorized);
};
let (id, name, email) = (row.get(0)?, row.get(1)?, row.get(2)?);
drop(rows);
let mut rows = conn
.query(
"SELECT r.name FROM roles r
JOIN user_roles ur ON ur.role_id = r.id
WHERE ur.user_id = ?1 ORDER BY r.name",
(id,),
)
.await?;
let mut roles = Vec::new();
while let Some(row) = rows.next().await? {
roles.push(row.get(0)?);
}
Ok(Self {
id,
name,
email,
roles,
token_hash,
})
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn is_admin(&self) -> bool {
self.has_role(ADMIN_ROLE)
}
/// Fails with `Forbidden` unless this user is an admin.
pub fn require_admin(&self) -> AppResult<()> {
if self.is_admin() {
Ok(())
} else {
Err(AppError::Forbidden)
}
}
/// Fails with `Forbidden` unless this user is `user_id` or an admin.
pub fn require_self_or_admin(&self, user_id: i64) -> AppResult<()> {
if self.id == user_id || self.is_admin() {
Ok(())
} else {
Err(AppError::Forbidden)
}
}
/// Stored hash of the token this call was authenticated with.
pub fn token_hash(&self) -> &str {
&self.token_hash
}
}
fn bearer_token(headers: &HeaderMap) -> Option<&str> {
let value = headers.get(AUTHORIZATION)?.to_str().ok()?;
let (scheme, token) = value.split_once(' ')?;
let token = token.trim();
(scheme.eq_ignore_ascii_case("bearer") && !token.is_empty()).then_some(token)
}