Direct Credentials Flow
The Credentials Flow enables traditional username/password authentication without relying on external OAuth providers.
Overview
While OAuth is recommended for most applications, sometimes you need direct
authentication with credentials stored in your own database. Authkestra's
CredentialsFlow provides a type-safe way to implement this.
CredentialsProvider
You must implement the CredentialsProvider trait. This trait is responsible for validating the user's input and returning an Identity.
#[async_trait]
pub trait CredentialsProvider: Send + Sync {
type Credentials: DeserializeOwned + Send;
async fn authenticate(&self, creds: Self::Credentials) -> Result<Identity, AuthError>;
}Actix-web Implementation
In Actix-web, you typically wrap the flow in your app state and call it from a login handler.
// 1. Define your login form
#[derive(Deserialize)]
pub struct LoginCredentials {
pub username: String,
pub password: String,
}
// 2. Implementation in your route handler
#[post("/login")]
async fn login(data: web::Data<AppState>, creds: web::Form<LoginCredentials>) -> impl Responder {
let (identity, _local_user) = data.auth_flow.authenticate(creds.into_inner()).await
.map_err(|e| HttpResponse::Unauthorized().body(e.to_string()))?;
// Create and save session
let session = Session::new(identity, chrono::Duration::hours(24));
data.authkestra.session_store.save_session(&session).await?;
let cookie = authkestra_actix::helpers::create_actix_cookie(&data.authkestra.session_config, session.id);
HttpResponse::Found()
.append_header(("Location", "/protected"))
.cookie(cookie)
.finish()
}Axum Implementation
The Axum implementation follows a similar pattern using extractors.
async fn login(
State(state): State<AppState>,
cookies: Cookies,
Form(creds): Form<LoginCredentials>,
) -> Result<impl IntoResponse, AuthError> {
let (identity, _local_user) = state.auth_flow.authenticate(creds).await?;
let session = Session::new(identity, chrono::Duration::hours(24));
state.authkestra.session_store.save_session(&session).await?;
let cookie = authkestra_axum::helpers::create_axum_cookie(
&state.authkestra.session_config,
session.id,
);
cookies.add(cookie);
Ok(Redirect::to("/protected"))
}Security Best Practices
Password Security
Never store plain-text passwords! Always use a secure hashing algorithm like bcrypt or argon2.
impl CredentialsProvider for DatabaseCredentialsProvider {
type Credentials = LoginCredentials;
async fn authenticate(&self, creds: Self::Credentials) -> Result<Identity, AuthError> {
let user = fetch_user_from_db(&creds.username).await?;
// Verify password hash
let valid = argon2::verify_encoded(&user.password_hash, creds.password.as_bytes())
.map_err(|_| AuthError::Internal("Hash verification failed".to_string()))?;
if !valid {
return Err(AuthError::InvalidCredentials);
}
Ok(Identity::new("credentials", user.id, user.email, Some(user.username)))
}
}Additional Security
- Implement rate limiting to prevent brute force attacks.
- Add account lockout after failed attempts.
- Use HTTPS in production.
- Consider adding 2FA support.
Full Examples
For complete working implementations including full setup, trait implementations, and error handling, see the following examples in the repository:
