Skip to main content

better_auth/
lib.rs

1//! # Better Auth - Rust
2//!
3//! A comprehensive authentication framework for Rust, inspired by Better-Auth.
4//!
5//! ## Quick Start
6//!
7//! ```rust,no_run
8//! use better_auth::{AuthBuilder, AuthConfig};
9//! use better_auth::adapters::MemoryDatabaseAdapter;
10//! use better_auth::plugins::EmailPasswordPlugin;
11//!
12//! #[tokio::main]
13//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
14//!     let config = AuthConfig::new("your-secret-key-that-is-at-least-32-chars");
15//!
16//!     let auth = AuthBuilder::new(config)
17//!         .database(MemoryDatabaseAdapter::new())
18//!         .plugin(EmailPasswordPlugin::new())
19//!         .build()
20//!         .await?;
21//!
22//!     Ok(())
23//! }
24//! ```
25
26// Core module — BetterAuth struct lives here in the root crate
27// because it orchestrates plugins (from better-auth-api) + core (from better-auth-core)
28pub mod core;
29pub mod handlers;
30
31// Re-export core abstractions
32pub use better_auth_core as types_mod;
33pub use better_auth_core::{
34    Account, AccountOps, Argon2Config, AuthConfig, AuthContext, AuthError, AuthPlugin, AuthRequest,
35    AuthResponse, AuthResult, AuthRoute, BodyLimitConfig, BodyLimitMiddleware, CacheAdapter,
36    ConsoleEmailProvider, CorsConfig, CorsMiddleware, CreateAccount, CreateInvitation,
37    CreateMember, CreateOrganization, CreatePasskey, CreateSession, CreateUser, CreateVerification,
38    CsrfConfig, CsrfMiddleware, DatabaseAdapter, DatabaseError, DatabaseHooks, DeleteUserResponse,
39    EmailProvider, EndpointRateLimit, HookedDatabaseAdapter, HttpMethod, Invitation, InvitationOps,
40    InvitationStatus, JwtConfig, MemberOps, MemoryAccount, MemoryCacheAdapter,
41    MemoryDatabaseAdapter, MemoryInvitation, MemoryMember, MemoryOrganization, MemorySession,
42    MemoryUser, MemoryVerification, Middleware, OpenApiBuilder, OpenApiSpec, OrganizationOps,
43    Passkey, PasskeyOps, PasswordConfig, RateLimitConfig, RateLimitMiddleware, SameSite, Session,
44    SessionConfig, SessionManager, SessionOps, TwoFactor, UpdateOrganization, UpdatePasskey,
45    UpdateUser, UpdateUserRequest, UpdateUserResponse, User, UserOps, Verification,
46    VerificationOps,
47};
48
49// Re-export entity traits
50pub use better_auth_core::entity::{
51    AuthAccount, AuthInvitation, AuthMember, AuthOrganization, AuthPasskey, AuthSession,
52    AuthTwoFactor, AuthUser, AuthVerification, MemberUserView,
53};
54
55// Re-export types under `types` module for backwards compatibility
56pub mod types {
57    pub use better_auth_core::{
58        Account, AuthRequest, AuthResponse, CreateAccount, CreateInvitation, CreateMember,
59        CreateOrganization, CreatePasskey, CreateSession, CreateUser, CreateVerification,
60        DeleteUserResponse, HttpMethod, Invitation, InvitationStatus, Passkey, Session, TwoFactor,
61        UpdateOrganization, UpdatePasskey, UpdateUser, UpdateUserRequest, UpdateUserResponse, User,
62        Verification,
63    };
64}
65
66// Re-export adapters
67pub mod adapters {
68    pub use better_auth_core::{
69        AccountOps, CacheAdapter, DatabaseAdapter, InvitationOps, MemberOps, MemoryAccount,
70        MemoryCacheAdapter, MemoryDatabaseAdapter, MemoryInvitation, MemoryMember,
71        MemoryOrganization, MemoryPasskey, MemorySession, MemoryUser, MemoryVerification,
72        OrganizationOps, PasskeyOps, SessionOps, UserOps, VerificationOps,
73    };
74
75    #[cfg(feature = "sqlx-postgres")]
76    pub use better_auth_core::adapters::database::sqlx_adapter::{
77        PoolConfig, PoolStats, SqlxAdapter, SqlxEntity,
78    };
79}
80
81// Re-export plugins
82pub mod plugins {
83    pub use better_auth_api::plugins::*;
84    pub use better_auth_api::*;
85}
86
87// Re-export the main BetterAuth struct
88pub use core::{AuthBuilder, BetterAuth, TypedAuthBuilder};
89
90#[cfg(feature = "axum")]
91pub use handlers::axum::AxumIntegration;
92
93#[cfg(test)]
94#[path = "tests/response_shape_tests.rs"]
95mod response_shape_tests;
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use serde_json::json;
101
102    fn test_config() -> AuthConfig {
103        AuthConfig::new("test-secret-key-that-is-at-least-32-characters-long")
104            .base_url("http://localhost:3000")
105            .password_min_length(8)
106    }
107
108    async fn create_test_auth() -> BetterAuth<MemoryDatabaseAdapter> {
109        AuthBuilder::new(test_config())
110            .database(MemoryDatabaseAdapter::new())
111            .plugin(plugins::EmailPasswordPlugin::new().enable_signup(true))
112            .build()
113            .await
114            .expect("Failed to create test auth instance")
115    }
116
117    #[tokio::test]
118    async fn test_auth_builder() {
119        let auth = create_test_auth().await;
120        assert_eq!(auth.plugin_names(), vec!["email-password"]);
121        assert_eq!(
122            auth.config().secret,
123            "test-secret-key-that-is-at-least-32-characters-long"
124        );
125    }
126
127    #[tokio::test]
128    async fn test_signup_flow() {
129        let auth = create_test_auth().await;
130
131        let signup_data = json!({
132            "email": "test@example.com",
133            "password": "password123",
134            "name": "Test User"
135        });
136
137        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
138        request.body = Some(signup_data.to_string().into_bytes());
139        request
140            .headers
141            .insert("content-type".to_string(), "application/json".to_string());
142
143        let response = auth
144            .handle_request(request)
145            .await
146            .expect("Signup request failed");
147
148        assert_eq!(response.status, 200);
149
150        let response_json: serde_json::Value =
151            serde_json::from_slice(&response.body).expect("Failed to parse response JSON");
152
153        assert!(response_json["user"]["id"].is_string());
154        assert_eq!(response_json["user"]["email"], "test@example.com");
155        assert_eq!(response_json["user"]["name"], "Test User");
156        assert!(response_json["token"].is_string());
157    }
158
159    #[tokio::test]
160    async fn test_signin_flow() {
161        let auth = create_test_auth().await;
162
163        let signup_data = json!({
164            "email": "signin@example.com",
165            "password": "password123",
166            "name": "Signin User"
167        });
168
169        let mut signup_request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
170        signup_request.body = Some(signup_data.to_string().into_bytes());
171        signup_request
172            .headers
173            .insert("content-type".to_string(), "application/json".to_string());
174
175        let signup_response = auth
176            .handle_request(signup_request)
177            .await
178            .expect("Signup failed");
179        assert_eq!(signup_response.status, 200);
180
181        let signin_data = json!({
182            "email": "signin@example.com",
183            "password": "password123"
184        });
185
186        let mut signin_request = AuthRequest::new(HttpMethod::Post, "/sign-in/email");
187        signin_request.body = Some(signin_data.to_string().into_bytes());
188        signin_request
189            .headers
190            .insert("content-type".to_string(), "application/json".to_string());
191
192        let signin_response = auth
193            .handle_request(signin_request)
194            .await
195            .expect("Signin failed");
196        assert_eq!(signin_response.status, 200);
197
198        let response_json: serde_json::Value =
199            serde_json::from_slice(&signin_response.body).expect("Failed to parse signin response");
200
201        assert_eq!(response_json["user"]["email"], "signin@example.com");
202        assert!(response_json["token"].is_string());
203    }
204
205    #[tokio::test]
206    async fn test_duplicate_email_signup() {
207        let auth = create_test_auth().await;
208
209        let signup_data = json!({
210            "name": "Duplicate User",
211            "email": "duplicate@example.com",
212            "password": "password123"
213        });
214
215        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
216        request.body = Some(signup_data.to_string().into_bytes());
217        request
218            .headers
219            .insert("content-type".to_string(), "application/json".to_string());
220
221        let response1 = auth
222            .handle_request(request.clone())
223            .await
224            .expect("First signup failed");
225        assert_eq!(response1.status, 200);
226
227        let response2 = auth
228            .handle_request(request)
229            .await
230            .expect("Second signup request failed");
231        assert_eq!(response2.status, 409);
232    }
233
234    #[tokio::test]
235    async fn test_invalid_credentials_signin() {
236        let auth = create_test_auth().await;
237
238        let signin_data = json!({
239            "email": "nonexistent@example.com",
240            "password": "password123"
241        });
242
243        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-in/email");
244        request.body = Some(signin_data.to_string().into_bytes());
245        request
246            .headers
247            .insert("content-type".to_string(), "application/json".to_string());
248
249        let response = auth
250            .handle_request(request)
251            .await
252            .expect("Request should not panic");
253        assert_eq!(response.status, 401);
254    }
255
256    #[tokio::test]
257    async fn test_weak_password_validation() {
258        let auth = create_test_auth().await;
259
260        let signup_data = json!({
261            "email": "weak@example.com",
262            "password": "123",
263            "name": "Weak Password User"
264        });
265
266        let mut request = AuthRequest::new(HttpMethod::Post, "/sign-up/email");
267        request.body = Some(signup_data.to_string().into_bytes());
268        request
269            .headers
270            .insert("content-type".to_string(), "application/json".to_string());
271
272        let response = auth
273            .handle_request(request)
274            .await
275            .expect("Request should not panic");
276        assert_eq!(response.status, 400);
277
278        let response_json: serde_json::Value =
279            serde_json::from_slice(&response.body).expect("Failed to parse response");
280        assert!(
281            response_json["message"]
282                .as_str()
283                .unwrap_or("")
284                .contains("Password must be at least")
285        );
286    }
287
288    #[tokio::test]
289    async fn test_session_management() {
290        let auth = create_test_auth().await;
291        let session_manager = auth.session_manager();
292
293        let database = auth.database();
294        let create_user = CreateUser::new()
295            .with_email("session@example.com")
296            .with_name("Session User");
297
298        let user = database
299            .create_user(create_user)
300            .await
301            .expect("Failed to create user");
302
303        let session = session_manager
304            .create_session(&user, None, None)
305            .await
306            .expect("Failed to create session");
307
308        assert!(session.token.starts_with("session_"));
309        assert_eq!(session.user_id, user.id);
310        assert!(session.active);
311
312        let retrieved_session = session_manager
313            .get_session(&session.token)
314            .await
315            .expect("Failed to get session")
316            .expect("Session not found");
317
318        assert_eq!(retrieved_session.id, session.id);
319        assert_eq!(retrieved_session.user_id, user.id);
320
321        session_manager
322            .delete_session(&session.token)
323            .await
324            .expect("Failed to delete session");
325
326        let deleted_session = session_manager
327            .get_session(&session.token)
328            .await
329            .expect("Failed to check deleted session");
330        assert!(deleted_session.is_none());
331    }
332
333    #[tokio::test]
334    async fn test_token_format_validation() {
335        let auth = create_test_auth().await;
336        let session_manager = auth.session_manager();
337
338        assert!(
339            session_manager
340                .validate_token_format("session_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN")
341        );
342        assert!(!session_manager.validate_token_format("invalid_token"));
343        assert!(!session_manager.validate_token_format("session_short"));
344        assert!(!session_manager.validate_token_format(""));
345    }
346
347    #[tokio::test]
348    async fn test_health_check_route() {
349        let auth = create_test_auth().await;
350
351        let request = AuthRequest::new(HttpMethod::Get, "/health");
352        let response = auth
353            .handle_request(request)
354            .await
355            .expect("Health check failed");
356
357        assert_eq!(response.status, 404);
358    }
359
360    #[tokio::test]
361    async fn test_config_validation() {
362        let config = AuthConfig::new("");
363        assert!(config.validate().is_err());
364
365        let config = AuthConfig::new("short");
366        assert!(config.validate().is_err());
367
368        let config = AuthConfig::new("this-is-a-valid-32-character-secret-key");
369        assert!(config.validate().is_ok());
370    }
371}