Core Traits
Authkestra uses traits to define extension points. Implement these traits to customize authentication behavior or add new providers.
OAuthProvider Trait
The OAuthProvider trait defines the interface for OAuth2/OIDC
providers. Implement this to add support for new identity providers.
/// Trait for an OAuth2-compatible provider.
#[async_trait]
pub trait OAuthProvider: Send + Sync {
/// Get the provider identifier.
fn provider_id(&self) -> &str;
/// Helper to get the authorization URL.
fn get_authorization_url(
&self,
state: &str,
scopes: &[&str],
code_challenge: Option<&str>,
) -> String;
/// Exchange an authorization code for an Identity.
async fn exchange_code_for_identity(
&self,
code: &str,
code_verifier: Option<&str>,
) -> Result<(Identity, OAuthToken), AuthError>;
/// Refresh an access token using a refresh token.
async fn refresh_token(&self, _refresh_token: &str) -> Result<OAuthToken, AuthError> {
Err(AuthError::Provider(
"Token refresh not supported by this provider".into(),
))
}
/// Revoke an access token.
async fn revoke_token(&self, _token: &str) -> Result<(), AuthError> {
Err(AuthError::Provider(
"Token revocation not supported by this provider".into(),
))
}
}Built-in Providers
Authkestra includes implementations for GitHub, Google, and Discord. You can use these directly or as reference when implementing custom providers.
CredentialsProvider Trait
For username/password or other direct authentication methods, implement the
CredentialsProvider trait:
use async_trait::async_trait;
use authkestra_core::{error::AuthError, state::Identity, CredentialsProvider};
use serde::Deserialize;
use std::collections::HashMap;
// 1. Define your credentials structure
#[derive(Deserialize)]
pub struct LoginCredentials {
pub username: String,
pub password: String,
}
// 2. Implement the CredentialsProvider trait
pub struct MyCredentialsProvider;
#[async_trait]
impl CredentialsProvider for MyCredentialsProvider {
type Credentials = LoginCredentials;
async fn authenticate(&self, creds: Self::Credentials) -> Result<Identity, AuthError> {
// In production: verify against your database with proper password hashing!
if creds.username == "admin" && creds.password == "secret" {
Ok(Identity {
provider_id: "credentials".to_string(),
external_id: "user-123".to_string(),
email: Some("admin@example.com".to_string()),
username: Some("admin".to_string()),
attributes: HashMap::new(),
})
} else {
Err(AuthError::InvalidCredentials)
}
}
}Password Security
Always use proper password hashing (bcrypt, argon2) in production. Never store or compare plain-text passwords.
UserMapper Trait
The UserMapper trait transforms an Identity into
your application's local user type:
use async_trait::async_trait;
use authkestra_core::{error::AuthError, state::Identity, UserMapper};
// Your application's user type
#[derive(Debug, Clone)]
pub struct LocalUser {
pub id: i64,
pub username: String,
pub role: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
pub struct DatabaseUserMapper {
pub pool: sqlx::PgPool,
}
#[async_trait]
impl UserMapper for DatabaseUserMapper {
type LocalUser = LocalUser;
async fn map_user(&self, identity: &Identity) -> Result<Self::LocalUser, AuthError> {
// Find or create user in database
let user = sqlx::query_as!(
LocalUser,
r#"
INSERT INTO users (external_id, provider, username, email)
VALUES ($1, $2, $3, $4)
ON CONFLICT (external_id, provider)
DO UPDATE SET last_login = NOW()
RETURNING id, username, role, created_at
"#,
identity.external_id,
identity.provider_id,
identity.username,
identity.email
)
.fetch_one(&self.pool)
.await
.map_err(|e| AuthError::Internal(e.to_string()))?;
Ok(user)
}
}ErasedOAuthFlow Trait
The ErasedOAuthFlow trait provides a type-erased interface for orchestrating the OAuth2 Authorization Code flow. This is particularly useful when you need to handle multiple different OAuth providers dynamically at runtime.
#[async_trait]
pub trait ErasedOAuthFlow: Send + Sync {
/// Get the provider identifier.
fn provider_id(&self) -> String;
/// Generates the redirect URL and CSRF state.
fn initiate_login(
&self,
scopes: &[&str],
pkce_challenge: Option<&str>
) -> (String, String);
/// Completes the flow by exchanging the code.
async fn finalize_login(
&self,
code: &str,
received_state: &str,
expected_state: &str,
pkce_verifier: Option<&str>,
) -> Result<(Identity, OAuthToken), AuthError>;
}ErasedOAuthFlow is implemented for Arc<T> and Box<T> where T: ErasedOAuthFlow, allowing for easy dynamic dispatch.
