axum-login

repository·main·Indexed 21 days ago

https://github.com/maxcountryman/axum-login

A Rust library providing user identification, authentication, and authorization for the Axum web framework. It features session management via AuthManagerLayer, flexible backend implementation through AuthnBackend and AuthzBackend traits, and route protection using the Require builder or convenience macros like login_required!.

Tokens
8.1K
Snippets
27
Records
33
Agent score
72%

What's inside axum-login

  1. Implement the core authentication traits

    main

    To use axum-login, you must implement two primary traits for your user type and your backend:

    1. AuthUser: Implemented on your user struct. It requires defining a unique Id type and a session_auth_hash (used for session validation).
    2. AuthnBackend: Implemented on your backend struct. It defines how to authenticate credentials and how to get_user via a UserId.

    This allows you to use any user type and any backend (Database, LDAP, etc.).

    use axum_login::{AuthUser, AuthnBackend, UserId};
    
    #[derive(Clone, Debug)]
    struct User;
    
    impl AuthUser for User {
        type Id = i64;
    
        fn id(&self) -> Self::Id {
            0
        }
    
        fn session_auth_hash(&self) -> &[u8] {
            &[]
        }
    }
    
    #[derive(Clone)]
    struct Backend;
    
    impl AuthnBackend for Backend {
        type User = User;
        type Credentials = ();
        type Error = std::convert::Infallible;
    
        async fn authenticate(
            &self,
            _: Self::Credentials,
        ) -> Result<Option<Self::User>, Self::Error> {
            Ok(Some(User))
        }
    
        async fn get_user(
            &self,
            _: &UserId<Self>,
        ) -> Result<Option<Self::User>, Self::Error> {
            Ok(Some(User))
        }
    }
  2. Protect routes using the Require builder

    main

    The Require builder is the primary way to protect routes. You can configure how unauthenticated users are handled (e.g., via RedirectHandler) and customize access logic using .decision().

    To use the builder, ensure the require-builder feature is enabled.

    Behavior Contract:

    • Unauthenticated: If the user is not logged in, the unauthenticated handler is used.
    • Unauthorized: If the user is logged in but lacks permissions, the unauthorized handler is used.
    • Redirects: Redirect fallbacks preserve existing redirect query parameters or append the configured redirect field. Errors in redirect construction return 500 Internal Server Error.
    use axum_login::require::{RedirectHandler, Require};
    use axum_login::{AuthUser, AuthnBackend, UserId};
    
    // ... (Implement AuthUser and AuthnBackend as shown above) ...
    
    let require = Require::<Backend>::builder()
        .unauthenticated(RedirectHandler::new().login_url("/login"))
        .build();
  3. Install axum-login

    main

    To use axum-login in your project, add it to your Cargo.toml dependencies.

    By default, it includes the macros-middleware feature. If you only want the builder-based middleware without the convenience macros, disable default features and enable require-builder explicitly.

    [dependencies]
    axum-login = "0.18.0"

    Or, for builder only (no macros):

    axum-login = { version = "0.18.0", default-features = false, features = ["require-builder"] }

  4. Customize unauthenticated and unauthorized responses

    main

    When using axum-login middleware, you can customize how the system responds when a user is unauthenticated (not logged in) or unauthorized (insufficient permissions) by providing a ResponseHandler.

    There are three main ways to implement a handler:

    1. RedirectHandler: Redirects unauthenticated users to a login page, typically appending the current URI as a query parameter so they can be returned to after signing in.
    2. SimpleResponseHandler: Returns a static response with a specific status code, body, content type, and custom headers (e.g., returning JSON or plain text).
    3. Closures: Any async closure that takes a Request and returns a type implementing IntoResponse can be used as a handler.
    // Example using RedirectHandler with RequireBuilder
    let require = Require::<Backend>::builder()
        .unauthenticated(
            RedirectHandler::new()
                .login_url("/login")
                .redirect_field("next"),
        )
        .build();
    
    // Example using SimpleResponseHandler with RequireBuilder
    let require = Require::<Backend>::builder()
        .unauthenticated(SimpleResponseHandler::text(
            StatusCode::UNAUTHORIZED,
            "Sign in to continue",
        ))
        .build();
  5. How AuthManagerLayer provides AuthSession

    main

    The AuthManagerLayer is a Tower Layer that wraps your service. When a request passes through this layer, it performs the following logic:

    1. Extracts the Session: It looks for a tower_sessions::Session in the request extensions. If no session is found, it returns an INTERNAL_SERVER_ERROR.
    2. Creates AuthSession: It uses the provided AuthnBackend and data_key to attempt to reconstruct an AuthSession from the session data. If this fails, it returns an INTERNAL_SERVER_ERROR.
    3. Injects AuthSession: The resulting AuthSession is inserted into the request extensions.
    4. Tracing: It automatically creates a tracing span and attempts to record the user.id if a user is successfully identified.

    This allows any downstream Axum handler to access the authenticated user via req.extensions().get::<AuthSession>().

  6. Manage user authentication with AuthSession

    main

    The AuthSession struct is a specialized session wrapper used for identifying, authenticating, and authorizing users. It is generic over a type implementing AuthnBackend.

    Key workflows:

    • Authentication: Use .authenticate(creds) to verify credentials against the backend. It returns Ok(Some(user)) if valid, or Ok(None) if invalid.
    • Logging In: Once credentials are verified, use .login(user) to persist the user's identity in the session. This method automatically mitigates session fixation by cycling the session ID if the user was not previously logged in.
    • Logging Out: Use .logout() to clear the user from the session and flush the session state.
    • Retrieving User: Use .user() to check if a user is currently authenticated in the session.
    // Example workflow concept
    let user = auth_session.authenticate(credentials).await?;
    if let Some(user) = user {
        auth_session.login(&user).await?;
    }
    
    let current_user = auth_session.user().await;
  7. How RequireService enforces authorization

    main

    The RequireService is a Tower service used to enforce authentication and authorization requirements on incoming requests. It operates by checking for an AuthSession<B> in the request extensions.

    Its logic follows this flow:

    1. Check Authentication: If no AuthSession is found in the request extensions, it returns an Internal Server Error (via InternalErrorFallback).
    2. Evaluate Predicate: If an AuthSession is present, it executes the predicate defined in the Require layer to produce a Decision.
    3. Handle Decision:
      • Decision::Allow: The request is forwarded to the inner service. Note that the service will wait for the inner service to be poll_ready before calling it.
      • Decision::Unauthorized: The request is intercepted, and the unauthorized handler defined in the Require layer is executed.
      • Decision::Unauthenticated: The request is intercepted, and the unauthenticated handler defined in the Require layer is executed.

    This service ensures that authorization logic is decoupled from your main application handlers.

  8. Understand Decision outcomes in axum-login

    main

    When performing authorization checks, a predicate returns a Decision which determines the request flow. The possible outcomes are:

    • Decision::Allow: The request is permitted to proceed.
    • Decision::Unauthenticated: The user is not logged in.
    • Decision::Unauthorized: The user is logged in but lacks the necessary permissions or failed the authorization check.
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum Decision {
        Allow,
        Unauthenticated,
        Unauthorized,
    }
  9. Integrate authentication using AuthManagerLayerBuilder

    main

    To add authentication to your Axum application, use the AuthManagerLayerBuilder. This builder constructs an AuthManagerLayer which provides an AuthSession as a request extension.

    Key Steps:

    1. Provide an implementation of AuthnBackend.
    2. Provide a SessionManagerLayer (from tower-sessions).
    3. (Optional) Configure a custom data_key using .with_data_key(). If omitted, it defaults to "axum-login.data".
    4. Call .build() to get the layer.

    Once the layer is applied to your Axum router, you can access the AuthSession in your handlers via request extensions.

    let session_layer = SessionManagerLayer::new(store);
    let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer)
        .with_data_key("my-custom-key")
        .build();
    
    let app = Router::new()
        .route("/", get(handler))
        .layer(auth_layer);
  10. Set up the AuthManagerLayer

    main

    To integrate axum-login into your axum application, you must create an AuthManagerLayer using the AuthManagerLayerBuilder. This layer requires a session manager (from tower-sessions) and your AuthnBackend implementation. This layer attaches the AuthSession to requests as an extension.

    // Session layer
    let session_store = MemoryStore::default();
    let session_layer = SessionManagerLayer::new(session_store);
    
    // Auth service
    let backend = Backend::default();
    let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
    
    let app = Router::new()
        .route("/protected", get(protected_handler))
        .layer(auth_layer);
  11. Configure the data_key in AuthManagerLayerBuilder

    main

    The data_key is the key used within the session to store and retrieve authentication data.

    By default, the builder uses "axum-login.data". You can override this using the with_data_key method. This key must be a &'static str.

    // Using the default key
    let layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
    
    // Using a custom key
    let layer = AuthManagerLayerBuilder::new(backend, session_layer)
        .with_data_key("custom.auth_data")
        .build();
    let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer)
        .with_data_key("my-custom-key")
        .build();
  12. Configure axum-login feature flags

    main

    axum-login uses feature flags to control its middleware surface:

    FeatureDescription
    require-builderEnables the Require builder module (the primary middleware surface).
    macros-middlewareEnables login_required! and permission_required! convenience macros. (Enabled by default).

    To use only the builder without the macros, use:

    axum-login = { version = "0.18.0", default-features = false, features = ["require-builder"] }