Skip to content

Conversation

@hannesdejager
Copy link
Collaborator

@hannesdejager hannesdejager commented Dec 27, 2025

Split Authentication from User Detail Providing

| Related unFTP commit |

Overview

This PR refactors the authentication system to decouple authentication (verifying credentials) from user detail retrieval (obtaining full user information). The Authenticator trait is now non-generic and returns a Principal type instead of a generic User type.

Key Changes

1. Non-Generic Authenticator Trait

Before:

pub trait Authenticator<User> {
    async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<User, AuthenticationError>;
}

After:

pub trait Authenticator {
    async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<Principal, AuthenticationError>;
}

The Authenticator trait is now non-generic and returns a Principal (authenticated identity) instead of a full user object.

2. New Principal Type

Introduced a new Principal struct that represents the minimal authenticated user identity:

pub struct Principal {
    pub username: String,
}

This contains only the authenticated username, separating the authentication step from user detail retrieval.

3. New UserDetailProvider Trait

Introduced a new trait (copied and adapted from unFTP server) to convert a Principal into a full UserDetail implementation:

#[async_trait]
pub trait UserDetailProvider {
    type User: UserDetail;
    
    async fn provide_user_detail(&self, principal: &Principal) -> Result<Self::User, UserDetailError>;
}

This allows authentication and user detail lookup to be separated, enabling more flexible architectures.

4. New AuthenticationPipeline Struct

Introduced AuthenticationPipeline that combines an Authenticator and a UserDetailProvider:

pub struct AuthenticationPipeline<User> {
    authenticator: Arc<dyn Authenticator + Send + Sync>,
    user_provider: Arc<dyn UserDetailProvider<User = User> + Send + Sync>,
}

The pipeline provides a unified interface for the two-step authentication process:

  1. Authenticate the user (returns Principal)
  2. Retrieve user details (converts Principal to User: UserDetail)

5. Updated ServerBuilder

  • Added with_user_detail_provider() constructor method
  • The new() and with_authenticator() methods now automatically initializes DefaultUserDetailProvider

6. Added DefaultUserDetailProvider

A convenience implementation that returns DefaultUser for simple use cases:

pub struct DefaultUserDetailProvider;

impl UserDetailProvider for DefaultUserDetailProvider {
    type User = DefaultUser;
    // ...
}

Migration Guide

For Authenticator Implementations

Before:

impl Authenticator<DefaultUser> for MyAuthenticator {
    async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<DefaultUser, AuthenticationError> {
        // verify credentials
        Ok(DefaultUser {})
    }
}

After:

impl Authenticator for MyAuthenticator {
    async fn authenticate(&self, username: &str, creds: &Credentials) -> Result<Principal, AuthenticationError> {
        // verify credentials
        Ok(Principal { username: username.to_string() })
    }
}

For Server Setup

Before:

let server = ServerBuilder::new(storage)
    .with_authenticator(Arc::new(MyAuthenticator))
    .build()?;

After:

let server = ServerBuilder::new(storage)
    .with_authenticator(Arc::new(MyAuthenticator))
    .user_detail_provider(Arc::new(DefaultUserDetailProvider))
    .build()?;

Or use the new constructor:

let server = ServerBuilder::with_user_detail_provider(
    storage_generator,
    Arc::new(MyUserDetailProvider)
)
.authenticator(Arc::new(MyAuthenticator))
.build()?;

For Custom User Types

If you have a custom user type, you'll need to implement UserDetailProvider:

#[derive(Debug)]
struct MyUser {
    username: String,
    home: PathBuf,
}

impl UserDetail for MyUser {
    fn home(&self) -> Option<&Path> {
        Some(&self.home)
    }
}

impl Display for MyUser {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.username)
    }
}

#[derive(Debug)]
struct MyUserDetailProvider;

#[async_trait]
impl UserDetailProvider for MyUserDetailProvider {
    type User = MyUser;
    
    async fn provide_user_detail(&self, principal: &Principal) -> Result<MyUser, UserDetailError> {
        // Look up user details from database/configuration
        Ok(MyUser {
            username: principal.username.clone(),
            home: PathBuf::from("/home/") / &principal.username,
        })
    }
}

Updated Crates

All authentication crates have been updated:

  • unftp-auth-jsonfile - Updated to return Principal
  • unftp-auth-pam - Updated to return Principal
  • unftp-auth-rest - Updated to return Principal

Benefits

  1. Separation of Concerns: Authentication (verifying credentials) is now separate from user detail retrieval
  2. Flexibility: Different authenticators can work with different user detail providers
  3. Reusability: Authenticators are no longer tied to specific user types
  4. Testability: Easier to test authentication and user detail retrieval independently

Breaking Changes

  • Authenticator<User>Authenticator (non-generic)
  • Authenticator.authenticate() now returns Principal instead of User
  • All unftp-auth-* crates need to be updated to use the new trait signature

This commit adds documentation and types to support
decoupling authentication from user detail retrieval:

- Document Principal type: Added detailed documentation explaining that
  Principal represents the minimal authenticated identity (username) returned
  by Authenticator implementations

- Add UserDetailProvider trait: New async trait that converts a Principal
  into a full UserDetail implementation, allowing authentication to be
  separated from user detail lookup. Includes complete documentation with
  example implementation.

- Add UserDetailError enum: Error type for UserDetailProvider operations
  with variants for generic errors, user not found, and implementation-
  specific errors. Fully documented with descriptions.

- Update exports: Export Principal and UserDetailProvider from auth module
  to make them available to library users.

This infrastructure prepares for future changes to make Authenticator
non-generic by returning Principal instead of User types, enabling better
separation of concerns between authentication and authorization.
@hannesdejager hannesdejager changed the title Hannes/split authz Split Authenticators from the Subject Resolving (UserDetail Providing) concern Dec 27, 2025
@hannesdejager hannesdejager merged commit 2b593c6 into master Dec 28, 2025
8 checks passed
@hannesdejager hannesdejager deleted the hannes/split-authz branch December 28, 2025 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants