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