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