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