Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,338 changes: 769 additions & 569 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@ sqlx = { version = "0.8.6", features = [
sqlx-postgres = { version = "0.8.6", optional = true }
axum = { version = "0.8.4", optional = true }
tokio = { version = "1.47.1", features = ["full"], optional = true }
percent-encoding = { version = "2.3.2", optional = true }

[features]
bare = []
postgres = ["dep:sqlx", "dep:sqlx-postgres", "tokio"]
axum = ["dep:axum", "tokio"]
axum = ["dep:axum", "dep:percent-encoding", "tokio"]
web = ["axum"]
db = ["postgres"]
full = ["db", "web"]
Expand Down
14 changes: 8 additions & 6 deletions examples/web_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,14 @@
//! use std::sync::Arc;
//!
//! #[tokio::main]
//! async fn main() {
//! async fn main() -> std::io::Result<()> {
//! env_logger::init();
//! let auth_service = Arc::new(AuthService::default());
//! #[cfg(feature = "axum")]
//! start_server(auth_service).await;
//! start_server(auth_service, None).await?;
//! #[cfg(not(feature = "web"))]
//! println!("Please enable the 'web' feature to run the web server example.");
//! Ok(())
//! }
//! ```
//!
Expand All @@ -134,13 +135,14 @@ use std::sync::Arc;
/// - If the `web` feature is enabled, the server is started and listens on `0.0.0.0:3000`.
/// - If the `web` feature is not enabled, a message is printed to enable the feature.
///
/// # Panics
/// This function will panic if the Tokio runtime cannot be started.
/// # Returns
/// Returns [`std::io::Result<()>`]. Errors from starting the Tokio runtime or server are returned
/// to the caller rather than panicked.
#[tokio::main]
async fn main() {
async fn main() -> std::io::Result<()> {
// Initialize logging
env_logger::init();

let auth_service = Arc::new(AuthService::default());
start_server(auth_service, None).await;
start_server(auth_service, None).await
}
20 changes: 10 additions & 10 deletions src/core/credentials/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@
//!
//! ## Example
//!
//! ```rust
//! use crate::core::credentials::{Credentials, PlainPassword};
//! use crate::core::password::SecurePasswordManager;
//! ```rust,no_run
//! use authen::core::credentials::{Credentials, PlainPassword};
//! use authen::core::password::SecurePasswordManager;
//! use authen::error::AuthError;
//! # struct DummyManager;
//! # #[async_trait::async_trait]
//! # impl SecurePasswordManager for DummyManager {
//! # async fn hash_password(&self, password: &str) -> Result<String, ()> { Ok(password.to_owned()) }
//! # async fn verify_password(&self, password: &str, hash: &str) -> Result<bool, ()> { Ok(password == hash) }
//! # async fn hash_password(&self, password: &str) -> Result<String, AuthError> { Ok(password.to_owned()) }
//! # async fn verify_password(&self, password: &str, hash: &str) -> Result<bool, AuthError> { Ok(password == hash) }
//! # }
//! # #[tokio::main]
//! # async fn main() {
//! # tokio::runtime::Runtime::new().unwrap().block_on(async {
//! let manager = DummyManager;
//! let plain = PlainPassword::new("my_password").unwrap();
//! let plain = PlainPassword::new("my_password".to_string());
//! let creds = Credentials::from_plain_password(&manager, "user-1".to_string(), "user@example.com".to_string(), plain).await.unwrap();
//! assert!(creds.verify_password(&manager, &PlainPassword::new("my_password").unwrap()).await.unwrap());
//! # }
//! assert!(creds.verify_password(&manager, &PlainPassword::new("my_password".to_string())).await.unwrap());
//! # });
//! ```

pub mod plain_password;
Expand Down
6 changes: 3 additions & 3 deletions src/core/hash/argon2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
//!
//! ```rust
//! use authen::core::hash::argon2::Argon2Hasher;
//! use argon2::password_hash::SaltString;
//! use authen::core::hash::generate_secure_salt;
//!
//! let hasher = Argon2Hasher::new();
//! let password = b"mysecret";
//! let salt = SaltString::generate(&mut rand::thread_rng());
//! let salt = generate_secure_salt().unwrap();
//! let hash = hasher.hash(password, Some(&salt)).unwrap();
//! assert!(hasher.verify(password, &hash).unwrap());
//! ```
Expand All @@ -36,7 +36,7 @@ impl Argon2Hasher {
///
/// # Examples
///
/// ```rust
/// ```rust,no_run
/// use authen::core::hash::argon2::Argon2Hasher;
/// let hasher = Argon2Hasher::new();
/// ```
Expand Down
45 changes: 30 additions & 15 deletions src/core/oauth/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,19 @@
//!
//! ## Example Usage
//!
//! ```rust
//! use crate::core::oauth::{OAuth2Manager, OAuth2Provider, OAuth2Config};
//! ```rust,no_run
//! use authen::core::oauth::{manager::OAuth2Manager, store::{OAuth2Config, OAuth2Provider}};
//! use std::collections::HashMap;
//!
//! let mut configs = HashMap::new();
//! configs.insert(OAuth2Provider::Google, OAuth2Config::default_google());
//! configs.insert(OAuth2Provider::Google, OAuth2Config {
//! app_name: "example-app".to_string(),
//! client_id: "client-id".to_string(),
//! client_secret: "client-secret".to_string(),
//! redirect_callback_uri: "http://localhost:3000/oauth/google/callback".to_string(),
//! redirect_frontend_uri: "http://localhost:5173/auth/callback".to_string(),
//! additional_scopes: vec!["email".to_string()],
//! });
//! let manager = OAuth2Manager::new(configs);
//! ```

Expand Down Expand Up @@ -83,11 +90,18 @@ type ConfiguredBasicClient = oauth2::Client<
/// - Microsoft
///
/// # Example
/// ```rust
/// use crate::core::oauth::{OAuth2Manager, OAuth2Provider, OAuth2Config};
/// ```rust,no_run
/// use authen::core::oauth::{manager::OAuth2Manager, store::{OAuth2Config, OAuth2Provider}};
/// use std::collections::HashMap;
/// let mut configs = HashMap::new();
/// configs.insert(OAuth2Provider::Google, OAuth2Config::default_google());
/// configs.insert(OAuth2Provider::Google, OAuth2Config {
/// app_name: "example-app".to_string(),
/// client_id: "client-id".to_string(),
/// client_secret: "client-secret".to_string(),
/// redirect_callback_uri: "http://localhost:3000/oauth/google/callback".to_string(),
/// redirect_frontend_uri: "http://localhost:5173/auth/callback".to_string(),
/// additional_scopes: vec!["email".to_string()],
/// });
/// let manager = OAuth2Manager::new(configs);
/// ```
pub struct OAuth2Manager {
Expand Down Expand Up @@ -121,7 +135,7 @@ impl OAuth2Manager {
/// Returns [`AuthError::ConfigError`] if the provider configuration is missing or the client cannot be built.
///
/// # Example
/// ```rust
/// ```rust,ignore
/// let client = manager.get_http_client(OAuth2Provider::Google)?;
/// ```
fn get_http_client(&self, provider: OAuth2Provider) -> Result<Client, AuthError> {
Expand All @@ -144,7 +158,7 @@ impl OAuth2Manager {
/// Returns [`AuthError::ConfigError`] if the provider configuration is missing or contains invalid URLs.
///
/// # Example
/// ```rust
/// ```rust,ignore
/// let client = manager.get_client(OAuth2Provider::GitHub)?;
/// ```
pub fn get_client(&self, provider: OAuth2Provider) -> Result<ConfiguredBasicClient, AuthError> {
Expand Down Expand Up @@ -199,7 +213,7 @@ impl OAuth2Manager {
/// Returns [`OAuth2UserInfo`] on success, or [`AuthError`] if required fields are missing or the response is invalid.
///
/// # Example
/// ```rust
/// ```rust,ignore
/// let user_info = manager.parse_user_info(OAuth2Provider::Google, json_response).await?;
/// ```
pub async fn parse_user_info(
Expand Down Expand Up @@ -357,7 +371,7 @@ impl OAuth2Service for OAuth2Manager {
let client = self.get_client(provider)?;

let config = self.configs.get(&provider).unwrap();
// Dans ton code Rust, assure-toi de dédupliquer les scopes
// Collect default and additional scopes.
let mut all_scopes = provider
.default_scopes()
.into_iter()
Expand All @@ -369,7 +383,7 @@ impl OAuth2Service for OAuth2Manager {
}
all_scopes.extend(config.additional_scopes.clone());

// Déduplication des scopes
// Deduplicate scopes.
all_scopes.sort();
all_scopes.dedup();

Expand Down Expand Up @@ -581,7 +595,7 @@ impl OAuth2Service for OAuth2Manager {
&self,
provider: OAuth2Provider,
) -> Result<String, AuthError> {
self.get_redirect_frontend_uri(provider)
OAuth2Manager::get_redirect_frontend_uri(self, provider)
}
}

Expand All @@ -597,7 +611,7 @@ impl OAuth2Manager {
/// Returns the frontend redirect URI as a string, or an [`AuthError`] if the provider configuration is missing.
///
/// # Example
/// ```rust
/// ```rust,ignore
/// let uri = manager.get_redirect_frontend_uri(OAuth2Provider::Discord)?;
/// ```
pub fn get_redirect_frontend_uri(&self, provider: OAuth2Provider) -> Result<String, AuthError> {
Expand All @@ -619,7 +633,7 @@ impl OAuth2Manager {
/// The updated [`OAuth2UserInfo`] with the `user_id` field set.
///
/// # Example
/// ```rust
/// ```rust,ignore
/// let linked_info = OAuth2Manager::link_to_user(oauth_info, user_id);
/// ```
pub fn link_to_user(mut oauth_info: OAuth2UserInfo, user_id: String) -> OAuth2UserInfo {
Expand All @@ -634,7 +648,8 @@ impl OAuth2Manager {
/// This implementation is useful for testing or initializing the manager before loading provider configs.
///
/// # Example
/// ```rust
/// ```rust,no_run
/// use authen::core::oauth::manager::OAuth2Manager;
/// let manager = OAuth2Manager::default();
/// ```
impl Default for OAuth2Manager {
Expand Down
86 changes: 72 additions & 14 deletions src/core/oauth/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@
//!
//! ## Example Configuration
//!
//! ```rust
//! ```rust,no_run
//! use authen::core::oauth::store::OAuth2Config;
//!
//! let config = OAuth2Config {
//! app_name: "My App".to_string(),
//! client_id: "your-client-id".to_string(),
//! client_secret: "your-client-secret".to_string(),
//! redirect_uri: "https://api.myapp.com/oauth/google/callback".to_string(),
//! redirect_callback_uri: "https://api.myapp.com/oauth/google/callback".to_string(),
//! redirect_frontend_uri: "https://myapp.com/auth/callback".to_string(),
//! additional_scopes: vec!["profile".to_string()],
//! };
Expand Down Expand Up @@ -78,7 +78,8 @@ impl OAuth2Provider {
///
/// # Examples
///
/// ```rust
/// ```rust,no_run
/// use authen::core::oauth::store::OAuth2Provider;
/// let provider = OAuth2Provider::Google;
/// assert_eq!(provider.display_name(), "Google");
/// ```
Expand All @@ -95,7 +96,8 @@ impl OAuth2Provider {
///
/// # Examples
///
/// ```rust
/// ```rust,no_run
/// use authen::core::oauth::store::OAuth2Provider;
/// let scopes = OAuth2Provider::Google.default_scopes();
/// assert!(scopes.contains(&"email"));
/// ```
Expand All @@ -109,6 +111,20 @@ impl OAuth2Provider {
}
}

impl std::str::FromStr for OAuth2Provider {
type Err = String;

fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_lowercase().as_str() {
"google" => Ok(Self::Google),
"github" => Ok(Self::GitHub),
"discord" => Ok(Self::Discord),
"microsoft" => Ok(Self::Microsoft),
_ => Err("expected one of: google, github, discord, microsoft".to_string()),
}
}
}

/// Represents an OAuth2 token, including access and refresh tokens, expiration, and provider info.
///
/// This struct holds all relevant information about an OAuth2 token issued by a provider,
Expand Down Expand Up @@ -139,8 +155,17 @@ impl OAuth2Token {
///
/// # Examples
///
/// ```rust
/// let token = OAuth2Token { /* ... */ };
/// ```rust,no_run
/// use authen::core::oauth::store::{OAuth2Provider, OAuth2Token};
/// let token = OAuth2Token {
/// access_token: "access".to_string(),
/// refresh_token: Some("refresh".to_string()),
/// expires_at: Some(chrono::Utc::now().naive_utc()),
/// token_type: "Bearer".to_string(),
/// scope: None,
/// provider: OAuth2Provider::Google,
/// created_at: chrono::Utc::now().naive_utc(),
/// };
/// let expired = token.is_expired();
/// ```
pub fn is_expired(&self) -> bool {
Expand All @@ -160,8 +185,17 @@ impl OAuth2Token {
///
/// # Examples
///
/// ```rust
/// let token = OAuth2Token { /* ... */ };
/// ```rust,no_run
/// use authen::core::oauth::store::{OAuth2Provider, OAuth2Token};
/// let token = OAuth2Token {
/// access_token: "access".to_string(),
/// refresh_token: Some("refresh".to_string()),
/// expires_at: Some(chrono::Utc::now().naive_utc()),
/// token_type: "Bearer".to_string(),
/// scope: None,
/// provider: OAuth2Provider::Google,
/// created_at: chrono::Utc::now().naive_utc(),
/// };
/// let soon = token.expires_soon(60);
/// ```
pub fn expires_soon(&self, threshold_secs: u64) -> bool {
Expand Down Expand Up @@ -241,8 +275,16 @@ impl OAuth2Config {
///
/// # Examples
///
/// ```rust
/// let config = OAuth2Config { /* ... */ };
/// ```rust,no_run
/// use authen::core::oauth::store::{OAuth2Config, OAuth2Provider};
/// let config = OAuth2Config {
/// app_name: "My App".to_string(),
/// client_id: "your-client-id".to_string(),
/// client_secret: "your-client-secret".to_string(),
/// redirect_callback_uri: "https://api.myapp.com/oauth/google/callback".to_string(),
/// redirect_frontend_uri: "https://myapp.com/auth/callback".to_string(),
/// additional_scopes: vec!["profile".to_string()],
/// };
/// let url = config.auth_url(OAuth2Provider::Google);
/// ```
pub fn auth_url(&self, provider: OAuth2Provider) -> &'static str {
Expand All @@ -268,8 +310,16 @@ impl OAuth2Config {
///
/// # Examples
///
/// ```rust
/// let config = OAuth2Config { /* ... */ };
/// ```rust,no_run
/// use authen::core::oauth::store::{OAuth2Config, OAuth2Provider};
/// let config = OAuth2Config {
/// app_name: "My App".to_string(),
/// client_id: "your-client-id".to_string(),
/// client_secret: "your-client-secret".to_string(),
/// redirect_callback_uri: "https://api.myapp.com/oauth/google/callback".to_string(),
/// redirect_frontend_uri: "https://myapp.com/auth/callback".to_string(),
/// additional_scopes: vec!["profile".to_string()],
/// };
/// let url = config.token_url(OAuth2Provider::Google);
/// ```
pub fn token_url(&self, provider: OAuth2Provider) -> &'static str {
Expand All @@ -295,8 +345,16 @@ impl OAuth2Config {
///
/// # Examples
///
/// ```rust
/// let config = OAuth2Config { /* ... */ };
/// ```rust,no_run
/// use authen::core::oauth::store::{OAuth2Config, OAuth2Provider};
/// let config = OAuth2Config {
/// app_name: "My App".to_string(),
/// client_id: "your-client-id".to_string(),
/// client_secret: "your-client-secret".to_string(),
/// redirect_callback_uri: "https://api.myapp.com/oauth/google/callback".to_string(),
/// redirect_frontend_uri: "https://myapp.com/auth/callback".to_string(),
/// additional_scopes: vec!["profile".to_string()],
/// };
/// let url = config.user_info_url(OAuth2Provider::Google);
/// ```
pub fn user_info_url(&self, provider: OAuth2Provider) -> &'static str {
Expand Down
Loading
Loading