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