axum-login 0.5.0

🪪 Session-based user authentication for Axum.
Documentation

Session-based user authentication for Axum.

This crate provides a Tower middleware which creates a generic interface between authenticated sessions and arbitrary user types. With it, these authentication workflows are made easy:

  1. Logging users in,
  2. Logging users out,
  3. Accessing the current user within a route,
  4. Protecting access to a resource.

User storage is decoupled from authentication: any storage engine for which UserStore is implemented is supported. Likewise any user type which implements [AuthUser] may be used.

Sessions are provided via axum-sessions. The session layer must be installed before the authentication layer as the session will be used internally, i.e. to store the user's authentication state.

Users

In order for your user type to interoperate with this crate, you'll need to implement AuthUser for it. Generally this should be straightforward. The crate assumes you can provide a stable identifier of generic type UserId, as well as a password hash of type String. In the case of the latter, the semantics of re-authentication can be controlled: if this value changes, then the session becomes invalidated and the user must re-authenticate.

Roles

Optionally an arbitrary Role type may be provided. This allows applications to restrict route access based on a role a given user may have. Roles may be any type so long as they implement PartialOrd and PartialEq. The get_role method should be used for retrieving the current role of a given user. See login_with_role for role-based route protection.

Stores

User stores for sqlx are provided when the requisite feature flag is given. This allows applications which already leverage sqlx to make sure of these backends for user authentication. As an example, Postgres backends can be used via PostgresStore.

Example

Most applications will use this middleware via axum.

Note that the below example makes use of memory-based stores for demonstration purposes only: more likely an application would never use these stores in practice except to enable uses cases like testing.

use std::{collections::HashMap, sync::Arc};

use axum::{response::IntoResponse, routing::get, Extension, Router};
use axum_login::{
axum_sessions::{async_session::MemoryStore as SessionMemoryStore, SessionLayer},
extractors::AuthContext,
memory_store::MemoryStore as AuthMemoryStore,
secrecy::SecretVec,
AuthLayer, AuthUser, RequireAuthorizationLayer,
};
use rand::Rng;
use tokio::sync::RwLock;

#[derive(Debug, Clone)]
struct User {
id: usize,
name: String,
password_hash: String,
role: Role,
}

#[derive(Debug, Clone, PartialEq, PartialOrd)]
enum Role {
User,
Admin,
}

impl User {
fn get_rusty_user() -> Self {
Self {
id: 1,
name: "Ferris the Crab".to_string(),
password_hash: "password".to_string(),
role: Role::Admin,
}
}
}

impl AuthUser<usize, Role> for User {
fn get_id(&self) -> usize {
self.id
}

fn get_password_hash(&self) -> SecretVec<u8> {
SecretVec::new(self.password_hash.clone().into())
}

fn get_role(&self) -> Option<Role> {
Some(self.role.clone())
}
}

type Auth = AuthContext<usize, User, AuthMemoryStore<usize, User>, Role>;
type RequireAuth = RequireAuthorizationLayer<usize, User, Role>;

#[tokio::main]
async fn main() {
let secret = rand::thread_rng().gen::<[u8; 64]>();

let session_store = SessionMemoryStore::new();
let session_layer = SessionLayer::new(session_store, &secret);

let store = Arc::new(RwLock::new(HashMap::default()));
let user = User::get_rusty_user();

store.write().await.insert(user.get_id(), user);

let user_store = AuthMemoryStore::new(&store);
let auth_layer = AuthLayer::new(user_store, &secret);

async fn login_handler(mut auth: Auth) {
auth.login(&User::get_rusty_user()).await.unwrap();
}

async fn logout_handler(mut auth: Auth) {
dbg!("Logging out user: {}", &auth.current_user);
auth.logout().await;
}

async fn protected_handler(Extension(user): Extension<User>) -> impl IntoResponse {
format!("Logged in as: {}", user.name)
}

async fn admin_handler(Extension(user): Extension<User>) -> impl IntoResponse {
format!("Logged in as admin: {}", user.name)
}

let app = Router::new()
.route("/admin", get(admin_handler))
.route_layer(RequireAuth::login_with_role(Role::Admin..))
.route("/", get(protected_handler))
.route_layer(RequireAuth::login())
.route("/login", get(login_handler))
.route("/logout", get(logout_handler))
.layer(auth_layer)
.layer(session_layer);

axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}