In our previous blog post we covered how to implement axum’s SessionStore trait. In this post we will continue where we left off and implement UserStore
Before we dive into the implementation details, let’s first provide some background information on UserStore. It is a component of the axum-login crate that stores the authenticated user’s state. To use the UserStore, you first need to create an auth layer and then register and then register it with your application. The UserStore provides separation between the storage and authentication processes, enabling any user type to be stored as long as it implements an Auth User trait trait.
Now, let’s take a look at my implementation
fn get_id(&self) -> String {
let hash = twox_hash::xxh3::hash64(self.username.as_ref());
format!("{}", hash)
}
The get_id() function should always return a unique value that can be used to locate the user in the UserStore. In my implementation, I have used sled - a key-value store - to store user data. My approach is straightforward: I hash the username and use the resulting value as the unique identifier for each user.
fn get_password_hash(&self) -> axum_login::secrecy::SecretVec<u8> {
SecretVec::new(self.password_hash.clone().into())
}
The get_password() function, as its name implies, returns the password, but it is wrapped in a SecretVec. This type is part of the secrecy crate, which helps manage sensitive data.
Now, let’s take a closer look at my implementation of the UserStore trait. You can find the implementation here.
#[async_trait]
impl<User, Role> UserStore<Role> for SledUserStore<User, Role>
where
Role: PartialOrd + PartialEq + Clone + Send + Sync + 'static,
User: AuthUser<Role> + From<IVec> + Clone + Send + Sync + 'static,
{
type User = User;
async fn load_user(&self, user_id: &str) -> Result<Option<Self::User>, eyre::Report> {
let opt_user = self
.inner
.get(user_id)
.expect("failed to find data for user_id");
let user: Option<User> = opt_user.map(|u| u.into());
match user {
Some(u) => Ok(Some(u)),
None => Err(eyre::eyre!("Could not find user by user_id: {:?}", user_id)),
}
}
}
There are a few interesting things that I learned during the implementation of the UserStore trait that I would like to share.
Firstly, the UserStore is generic over both User and Role, although I did not have any specific requirements for the Role parameter. However, the associated User type is interesting in that it is bound to the AuthUser trait, which also appears in the return type Result<OptionSelf::User, eyre::Report>
.
Futhermore, I encountered some issues while using the get() method in the sled crate, which returns a Result<Option<IVec>>
that does not match the associated type AuthUser. After conducting some research, I was able to find a solution to make the compiler happy. You can find the solution in part 1 and part 2.
It took me some time to realize that I had used a similar pattern extensively in Scala, as shown in this example. In that code, the F defined there is a type (actually a type constructor, but it does not matter here) that is refined by adding more context, such as Sync, ContextShift, and so on. Another way to think about this is that we add more capabilities with each context bound.
Once I realized this, the addition of From<IVec>
solved the compiler issue. On a side note, I have used a similar pattern in Kotlin as well.
Secondly, I wanted to touch on the topic of Phantom Types, which I came across while implementing the UserStore trait. After reading about them, I quickly realized that I had used a similar pattern in Scala called the Tagged Type. This pattern involves attaching extra information to types that only exist at compile time and are earsed at runtime.
This was a really enjoyable exercise that allowed me to explore different parts of the Rust language.
“Rumi Quote”: Raise your words, not voice. It is rain that grows flowers, not thunder.