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