use std::sync::Arc;
use sea_orm::DatabaseConnection;
use warp::Filter;
use bitrouter_accounts::identity::{AccountId, Identity, Scope};
use bitrouter_accounts::service::AccountService;
use bitrouter_core::auth::chain::Caip10;
use bitrouter_core::auth::claims::TokenScope;
use bitrouter_core::auth::revocation::KeyRevocationSet;
use bitrouter_core::auth::token as jwt_token;
#[derive(Clone)]
pub struct JwtAuthContext {
db: Option<DatabaseConnection>,
operator_caip10: Option<String>,
revocation_set: Option<Arc<dyn KeyRevocationSet>>,
}
impl JwtAuthContext {
pub fn new(db: Option<DatabaseConnection>, operator_caip10: Option<String>) -> Self {
Self {
db,
operator_caip10,
revocation_set: None,
}
}
pub fn with_revocation_set(mut self, set: Arc<dyn KeyRevocationSet>) -> Self {
self.revocation_set = Some(set);
self
}
pub fn is_open(&self) -> bool {
self.db.is_none()
}
}
fn extract_bearer(header: &str) -> Option<&str> {
for segment in header.split(',') {
let trimmed = segment.trim();
if let Some(token) = trimmed
.strip_prefix("Bearer ")
.or_else(|| trimmed.strip_prefix("bearer "))
{
return Some(token.trim());
}
}
None
}
pub fn bearer_credential() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Clone {
warp::header::optional::<String>("authorization").and_then(
|header: Option<String>| async move {
match header.and_then(|h| extract_bearer(&h).map(str::to_owned)) {
Some(key) => Ok(key),
None => Err(warp::reject::custom(Unauthorized("missing bearer token"))),
}
},
)
}
pub fn any_credential() -> impl Filter<Extract = (String,), Error = warp::Rejection> + Clone {
warp::header::optional::<String>("authorization")
.and(warp::header::optional::<String>("x-api-key"))
.and_then(
|auth_header: Option<String>, x_api_key: Option<String>| async move {
if let Some(key) = auth_header.and_then(|h| extract_bearer(&h).map(str::to_owned)) {
return Ok(key);
}
if let Some(key) = x_api_key.filter(|k| !k.is_empty()) {
return Ok(key);
}
Err(warp::reject::custom(Unauthorized(
"missing authentication credentials",
)))
},
)
}
async fn resolve_identity(
credential: &str,
ctx: &JwtAuthContext,
) -> Result<Identity, warp::Rejection> {
resolve_jwt_identity(credential, ctx).await
}
async fn resolve_jwt_identity(
credential: &str,
ctx: &JwtAuthContext,
) -> Result<Identity, warp::Rejection> {
let claims = jwt_token::verify(credential)
.map_err(|_| warp::reject::custom(Unauthorized("invalid JWT signature")))?;
jwt_token::check_expiration(&claims)
.map_err(|_| warp::reject::custom(Unauthorized("JWT expired")))?;
if let Some(ref expected) = ctx.operator_caip10
&& claims.iss != *expected
{
return Err(warp::reject::custom(Unauthorized(
"JWT issuer does not match configured operator wallet",
)));
}
if let Some(ref key_id) = claims.id
&& let Some(ref revocation_set) = ctx.revocation_set
&& revocation_set.is_revoked(key_id).await
{
return Err(warp::reject::custom(Unauthorized(
"API key has been revoked",
)));
}
let chain = Caip10::parse(&claims.iss).ok().map(|c| c.chain.caip2());
let Some(ref db) = ctx.db else {
return Err(warp::reject::custom(Unauthorized(
"authentication requires a database",
)));
};
let svc = AccountService::new(db);
let account = svc
.find_or_create_by_pubkey(&claims.iss)
.await
.map_err(|_| warp::reject::custom(Unauthorized("account lookup failed")))?;
let Some(account) = account else {
return Err(warp::reject::custom(Unauthorized(
"public key has been rotated — generate a new token with your current key",
)));
};
let scope = match claims.scope() {
TokenScope::Admin => Scope::Admin,
TokenScope::Api => Scope::Api,
};
Ok(Identity {
account_id: AccountId(account.id),
scope,
key_id: claims.id,
chain,
models: claims.mdl,
budget: claims.bgt,
budget_scope: claims.bsc,
issued_at: claims.iat,
key: claims.key,
policy_id: claims.pol,
})
}
pub fn openai_auth(
ctx: Arc<JwtAuthContext>,
) -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
if ctx.is_open() {
return open_identity().boxed();
}
let ctx = ctx.clone();
bearer_credential()
.and(warp::any().map(move || ctx.clone()))
.and_then(|credential: String, ctx: Arc<JwtAuthContext>| async move {
resolve_identity(&credential, &ctx).await
})
.boxed()
}
pub fn anthropic_auth(
ctx: Arc<JwtAuthContext>,
) -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
if ctx.is_open() {
return open_identity().boxed();
}
let ctx = ctx.clone();
any_credential()
.and(warp::any().map(move || ctx.clone()))
.and_then(|credential: String, ctx: Arc<JwtAuthContext>| async move {
resolve_identity(&credential, &ctx).await
})
.boxed()
}
pub fn management_auth(
ctx: Arc<JwtAuthContext>,
) -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
if ctx.is_open() {
return open_identity().boxed();
}
let ctx = ctx.clone();
any_credential()
.and(warp::any().map(move || ctx.clone()))
.and_then(|credential: String, ctx: Arc<JwtAuthContext>| async move {
resolve_identity(&credential, &ctx).await
})
.boxed()
}
fn open_identity() -> impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone {
warp::any().and_then(|| async {
Ok::<_, warp::Rejection>(Identity {
account_id: AccountId(uuid::Uuid::nil()),
scope: Scope::Admin,
key_id: None,
chain: None,
models: None,
budget: None,
budget_scope: None,
issued_at: None,
key: None,
policy_id: None,
})
})
}
#[derive(Debug)]
pub struct Unauthorized(pub &'static str);
impl std::fmt::Display for Unauthorized {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "unauthorized: {}", self.0)
}
}
impl warp::reject::Reject for Unauthorized {}
pub(crate) fn auth_gate(
auth: impl Filter<Extract = (Identity,), Error = warp::Rejection> + Clone,
) -> impl Filter<Extract = (), Error = warp::Rejection> + Clone {
auth.map(|_| ()).untuple_one()
}