better-auth-core 0.10.0

Core abstractions for better-auth: traits, types, config, error handling
Documentation
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::Arc;

use crate::adapters::DatabaseAdapter;
use crate::config::AuthConfig;
use crate::email::EmailProvider;
use crate::entity::AuthSession;
use crate::error::{AuthError, AuthResult};
use crate::session::SessionManager;
use crate::types::{AuthRequest, AuthResponse, HttpMethod};

/// Action returned by [`AuthPlugin::before_request`].
#[derive(Debug)]
pub enum BeforeRequestAction {
    /// Short-circuit with this response (e.g. return session JSON).
    Respond(AuthResponse),
    /// Inject a virtual session so downstream handlers see it as authenticated.
    InjectSession {
        user_id: String,
        session_token: String,
    },
}

/// Plugin trait that all authentication plugins must implement.
///
/// Generic over `DB` so that lifecycle hooks receive the adapter's concrete
/// entity types (e.g., `DB::User`, `DB::Session`).
#[async_trait]
pub trait AuthPlugin<DB: DatabaseAdapter>: Send + Sync {
    /// Plugin name - should be unique
    fn name(&self) -> &'static str;

    /// Routes that this plugin handles
    fn routes(&self) -> Vec<AuthRoute>;

    /// Called when the plugin is initialized
    async fn on_init(&self, ctx: &mut AuthContext<DB>) -> AuthResult<()> {
        let _ = ctx;
        Ok(())
    }

    /// Called before route matching for every incoming request.
    ///
    /// Return `Some(BeforeRequestAction::Respond(..))` to short-circuit with a
    /// response, `Some(BeforeRequestAction::InjectSession { .. })` to attach a
    /// virtual session (e.g. API-key → session emulation), or `None` to let the
    /// request continue to normal route matching.
    async fn before_request(
        &self,
        _req: &AuthRequest,
        _ctx: &AuthContext<DB>,
    ) -> AuthResult<Option<BeforeRequestAction>> {
        Ok(None)
    }

    /// Called for each request - return Some(response) to handle, None to pass through
    async fn on_request(
        &self,
        req: &AuthRequest,
        ctx: &AuthContext<DB>,
    ) -> AuthResult<Option<AuthResponse>>;

    /// Called after a user is created
    async fn on_user_created(&self, user: &DB::User, ctx: &AuthContext<DB>) -> AuthResult<()> {
        let _ = (user, ctx);
        Ok(())
    }

    /// Called after a session is created
    async fn on_session_created(
        &self,
        session: &DB::Session,
        ctx: &AuthContext<DB>,
    ) -> AuthResult<()> {
        let _ = (session, ctx);
        Ok(())
    }

    /// Called before a user is deleted
    async fn on_user_deleted(&self, user_id: &str, ctx: &AuthContext<DB>) -> AuthResult<()> {
        let _ = (user_id, ctx);
        Ok(())
    }

    /// Called before a session is deleted
    async fn on_session_deleted(
        &self,
        session_token: &str,
        ctx: &AuthContext<DB>,
    ) -> AuthResult<()> {
        let _ = (session_token, ctx);
        Ok(())
    }
}

/// Generates the [`AuthPlugin<DB>`] impl for a plugin with static route dispatch.
///
/// Eliminates the dual declaration of routes in `routes()` and `on_request()`
/// by generating both from a single route table.
///
/// # Exceptions (must keep manual impl)
/// - `OAuthPlugin` — dynamic path matching for `/callback/{provider}`
/// - `SessionManagementPlugin` — match guards and OR patterns
/// - `EmailPasswordPlugin` — conditional routes based on config
/// - `UserManagementPlugin` — conditional routes based on config
/// - `PasswordManagementPlugin` — dynamic path matching for `/reset-password/{token}`
/// - `OrganizationPlugin` — handlers accept extra `&self.config` argument
#[macro_export]
macro_rules! impl_auth_plugin {
    (@pat get) => { $crate::HttpMethod::Get };
    (@pat post) => { $crate::HttpMethod::Post };
    (@pat put) => { $crate::HttpMethod::Put };
    (@pat delete) => { $crate::HttpMethod::Delete };
    (@pat patch) => { $crate::HttpMethod::Patch };
    (@pat head) => { $crate::HttpMethod::Head };

    (@route get) => { $crate::AuthRoute::get };
    (@route post) => { $crate::AuthRoute::post };
    (@route put) => { $crate::AuthRoute::put };
    (@route delete) => { $crate::AuthRoute::delete };

    (
        $plugin:ty, $name:expr;
        routes {
            $( $method:ident $path:literal => $handler:ident, $op_id:literal );* $(;)?
        }
        $( extra { $($extra:tt)* } )?
    ) => {
        #[::async_trait::async_trait]
        impl<DB: $crate::adapters::DatabaseAdapter> $crate::AuthPlugin<DB> for $plugin {
            fn name(&self) -> &'static str { $name }

            fn routes(&self) -> Vec<$crate::AuthRoute> {
                vec![
                    $( $crate::AuthRoute::new($crate::impl_auth_plugin!(@pat $method), $path, $op_id), )*
                ]
            }

            async fn on_request(
                &self,
                req: &$crate::AuthRequest,
                ctx: &$crate::AuthContext<DB>,
            ) -> $crate::AuthResult<Option<$crate::AuthResponse>> {
                match (req.method(), req.path()) {
                    $(
                        ($crate::impl_auth_plugin!(@pat $method), $path) => {
                            Ok(Some(self.$handler(req, ctx).await?))
                        }
                    )*
                    _ => Ok(None),
                }
            }

            $( $($extra)* )?
        }
    };
}

/// Route definition for plugins
#[derive(Debug, Clone)]
pub struct AuthRoute {
    pub path: String,
    pub method: HttpMethod,
    /// Identifier used as the OpenAPI `operationId` for this route.
    pub operation_id: String,
}

/// Context passed to plugin methods
pub struct AuthContext<DB: DatabaseAdapter> {
    pub config: Arc<AuthConfig>,
    pub database: Arc<DB>,
    pub email_provider: Option<Arc<dyn EmailProvider>>,
    pub metadata: HashMap<String, serde_json::Value>,
}

impl AuthRoute {
    pub fn new(
        method: HttpMethod,
        path: impl Into<String>,
        operation_id: impl Into<String>,
    ) -> Self {
        Self {
            path: path.into(),
            method,
            operation_id: operation_id.into(),
        }
    }

    pub fn get(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
        Self::new(HttpMethod::Get, path, operation_id)
    }

    pub fn post(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
        Self::new(HttpMethod::Post, path, operation_id)
    }

    pub fn put(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
        Self::new(HttpMethod::Put, path, operation_id)
    }

    pub fn delete(path: impl Into<String>, operation_id: impl Into<String>) -> Self {
        Self::new(HttpMethod::Delete, path, operation_id)
    }
}

impl<DB: DatabaseAdapter> AuthContext<DB> {
    pub fn new(config: Arc<AuthConfig>, database: Arc<DB>) -> Self {
        let email_provider = config.email_provider.clone();
        Self {
            config,
            database,
            email_provider,
            metadata: HashMap::new(),
        }
    }

    pub fn set_metadata(&mut self, key: impl Into<String>, value: serde_json::Value) {
        self.metadata.insert(key.into(), value);
    }

    pub fn get_metadata(&self, key: &str) -> Option<&serde_json::Value> {
        self.metadata.get(key)
    }

    /// Get the email provider, returning an error if none is configured.
    pub fn email_provider(&self) -> AuthResult<&dyn EmailProvider> {
        self.email_provider
            .as_deref()
            .ok_or_else(|| AuthError::config("No email provider configured"))
    }

    /// Create a `SessionManager` from this context's config and database.
    pub fn session_manager(&self) -> crate::session::SessionManager<DB> {
        crate::session::SessionManager::new(self.config.clone(), self.database.clone())
    }

    /// Extract a session token from the request, validate the session, and
    /// return the authenticated `(User, Session)` pair.
    ///
    /// This centralises the pattern previously duplicated across many plugins
    /// (`get_authenticated_user`, `require_session`, etc.).
    pub async fn require_session(&self, req: &AuthRequest) -> AuthResult<(DB::User, DB::Session)> {
        let session_manager = self.session_manager();

        if let Some(token) = session_manager.extract_session_token(req)
            && let Some(session) = session_manager.get_session(&token).await?
            && let Some(user) = self.database.get_user_by_id(session.user_id()).await?
        {
            return Ok((user, session));
        }

        Err(AuthError::Unauthenticated)
    }
}

/// Axum-friendly shared state type.
///
/// All fields are behind `Arc` so `AuthState` is cheap to clone and can
/// be used directly as axum `State`.
pub struct AuthState<DB: DatabaseAdapter> {
    pub config: Arc<AuthConfig>,
    pub database: Arc<DB>,
    pub session_manager: SessionManager<DB>,
    pub email_provider: Option<Arc<dyn EmailProvider>>,
}

impl<DB: DatabaseAdapter> Clone for AuthState<DB> {
    fn clone(&self) -> Self {
        Self {
            config: self.config.clone(),
            database: self.database.clone(),
            session_manager: self.session_manager.clone(),
            email_provider: self.email_provider.clone(),
        }
    }
}

impl<DB: DatabaseAdapter> AuthState<DB> {
    /// Create a new `AuthState` from an `AuthContext` and `SessionManager`.
    pub fn new(ctx: &AuthContext<DB>, session_manager: SessionManager<DB>) -> Self {
        Self {
            config: ctx.config.clone(),
            database: ctx.database.clone(),
            session_manager,
            email_provider: ctx.email_provider.clone(),
        }
    }

    /// Create an `AuthContext` for use with existing plugin handler methods.
    pub fn to_context(&self) -> AuthContext<DB> {
        let mut ctx = AuthContext::new(self.config.clone(), self.database.clone());
        ctx.email_provider = self.email_provider.clone();
        ctx
    }

    /// Build a `Set-Cookie` header value for a session token.
    pub fn session_cookie(&self, token: &str) -> String {
        crate::utils::cookie_utils::create_session_cookie(token, &self.config)
    }

    /// Build a `Set-Cookie` header value that clears the session cookie.
    pub fn clear_session_cookie(&self) -> String {
        crate::utils::cookie_utils::create_clear_session_cookie(&self.config)
    }
}

/// Plugin trait for axum-native routing.
///
/// Unlike [`AuthPlugin`] which uses the custom `AuthRequest`/`AuthResponse`
/// abstraction, `AxumPlugin` returns a standard `axum::Router` with handlers
/// already bound to routes. This eliminates the triple route-matching overhead
/// and enables use of axum extractors.
#[cfg(feature = "axum")]
#[async_trait]
pub trait AxumPlugin<DB: DatabaseAdapter>: Send + Sync {
    /// Plugin name — should be unique and match the `AuthPlugin` name when
    /// both traits are implemented on the same type.
    fn name(&self) -> &'static str;

    /// Return an axum `Router` with all routes for this plugin.
    ///
    /// The router uses `AuthState<DB>` as its state type.
    fn router(&self) -> axum::Router<AuthState<DB>>;

    /// Called after a user is created.
    async fn on_user_created(&self, _user: &DB::User, _ctx: &AuthContext<DB>) -> AuthResult<()> {
        Ok(())
    }

    /// Called after a session is created.
    async fn on_session_created(
        &self,
        _session: &DB::Session,
        _ctx: &AuthContext<DB>,
    ) -> AuthResult<()> {
        Ok(())
    }

    /// Called before a user is deleted.
    async fn on_user_deleted(&self, _user_id: &str, _ctx: &AuthContext<DB>) -> AuthResult<()> {
        Ok(())
    }

    /// Called before a session is deleted.
    async fn on_session_deleted(
        &self,
        _session_token: &str,
        _ctx: &AuthContext<DB>,
    ) -> AuthResult<()> {
        Ok(())
    }
}