rustrust-axumaxum-login

Lifetime errors implementing AuthnBackend for axum-login


I'm trying to implement AuthnBackend for the axum-login crate, and I can't figure out why the compiler is giving errors with this code. I've tried to stay really close to the sqlite example provided in the docs, except that I'm using the rusqlite crate in place of sqlx.

I can tell from the compiler errors that it's a lifetime error, but I'm not sure how to resolve it. The linked example doesn't explicitly state any lifetimes, and I'm not sure if I should need to here or how.

app/src/users.rs:

use std::{fmt, sync::Arc};

use axum_login::{AuthUser, AuthnBackend, UserId};
use password_auth::verify_password;
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use serde_cbor::from_slice;
use tokio::sync::Mutex;

#[derive(Clone, Deserialize, Serialize)]
pub struct User {
    pub id: u64,
    pub username: String,
    pub pw_hash: String,
    pub role: UserRole,
}

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

impl fmt::Debug for User {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{} (id {}, role {:?})",
            self.username, self.id, self.role
        )
    }
}

impl AuthUser for User {
    type Id = u64;

    fn id(&self) -> Self::Id {
        return self.id;
    }

    fn session_auth_hash(&self) -> &[u8] {
        return self.pw_hash.as_bytes();
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum UserRole {
    Admin = 2,
    User = 1,
    Guest = 0,
}

/// Extracted from authentication forms
#[derive(Debug, Clone, Deserialize)]
pub struct Credentials {
    pub username: String,
    pub password: String,
    pub next: Option<String>,
}

#[derive(Clone, Debug)]
pub struct Backend {
    db_con: Arc<Mutex<Connection>>,
}

impl Backend {
    pub fn new(db_con: Arc<Mutex<Connection>>) -> Self {
        return Backend { db_con };
    }
}

#[derive(Debug, thiserror::Error)]
pub enum UserBackendError {
    #[error(transparent)]
    Rusqlite(#[from] rusqlite::Error),

    #[error(transparent)]
    PasswordVerify(#[from] password_auth::VerifyError),
}

impl AuthnBackend for Backend {
    type User = User;
    type Credentials = Credentials;
    type Error = UserBackendError;

    async fn authenticate(
        &self,
        creds: Self::Credentials,
    ) -> Result<Option<Self::User>, Self::Error> {
        // Connect to db
        let conn = self.db_con.lock().await;

        // Load user data from db
        let user_data = match conn.query_row(
            "SELECT data FROM users WHERE username = ?1",
            [&creds.username],
            |row| row.get::<usize, Vec<u8>>(0),
        ) {
            Ok(data) => data,
            Err(err) => match err {
                rusqlite::Error::QueryReturnedNoRows => {
                    return Ok(None);
                }
                _ => return Err(self::UserBackendError::Rusqlite(err)),
            },
        };

        // Deserialize user data
        let user: User = from_slice(&user_data).unwrap();

        // Verify password
        match verify_password(creds.password, &user.pw_hash) {
            Ok(_) => return Ok(Some(user)),
            Err(err) => return Err(self::UserBackendError::PasswordVerify(err)),
        };
    }

    async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
        // Connect to db
        let conn = self.db_con.lock().await;

        // Load user data from db
        let user_data =
            match conn.query_row("SELECT data FROM users WHERE id = ?1", [user_id], |row| {
                row.get::<usize, Vec<u8>>(0)
            }) {
                Ok(data) => data,
                Err(err) => match err {
                    rusqlite::Error::QueryReturnedNoRows => {
                        return Ok(None);
                    }
                    _ => return Err(self::UserBackendError::Rusqlite(err)),
                },
            };

        // Deserialize user data
        let user: User = from_slice(&user_data).unwrap();
        return Ok(Some(user));
    }
}

Compiler errors:

error[E0195]: lifetime parameters or bounds on method `authenticate` do not match the trait declaration
  --> app/src/users.rs:86:26
   |
86 |     async fn authenticate(
   |                          ^ lifetimes do not match method in trait

error[E0195]: lifetime parameters or bounds on method `get_user` do not match the trait declaration
   --> app/src/users.rs:118:22
    |
118 |     async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
    |                      ^ lifetimes do not match method in trait

Solution

  • AuthnBackend uses async-trait. For a trait defined with #[async_trait], you need to use #[async_trait] on the implementation as well (or at least the desugared form). This is the case even though Rust now supports async in traits since the technique used by #[async_trait] (i.e. boxed futures) is different than native async trait methods (opaque types).

    So you should add it to your Backend implementation:

    [dependencies]
    async-trait = "0.1.88"
    
    use async_trait::async_trait;
    
    #[async_trait]
    impl AuthnBackend for Backend {
    

    For completeness, the docs show the desugared version looks like this:

    fn authenticate<'life0, 'async_trait>(
        &'life0 self,
        creds: Self::Credentials,
    ) -> Pin<Box<dyn Future<Output = Result<Option<Self::User>, Self::Error>> + Send + 'async_trait>>
       where Self: 'async_trait,
             'life0: 'async_trait;
    

    The example you link to doesn't have #[async_trait] since it appears the main branch has converted to native(-ish) async trait methods. The same example from the v0.17.0 branch - the latest published version - still uses #[async_trait].