Authkestra

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:

On this page