- Published on
Simple authentication with actix-web
- Authors
- Name
- Njuguna Mureithi
- @tweetofnjuguna
Overview
This article is based on actix-acl-example available on Github and How can I make protected routes in actix-web on StackOverflow
Getting Started
Our end goal is to achieve such an design:
#[get("/admin")]
async fn admin(admin: Admin) -> impl Responder {
HttpResponse::Ok().body("You are an admin")
}
#[get("/account")]
async fn account(user: User) -> impl Responder {
web::Json(user)
}
#[get("/team")]
async fn team(user: User) -> impl Responder {
if user.authorities != Scope::Admin {
return HttpResponse::Ok().json(vec![user])
}
let user_list = db.get_team();
HttpResponse::Ok().json(user_list)
}
This approach will allow you to apply authentication on multiple routes and also get the user details in return.
Defining posible types
Lets define our user and possible user types:
We are going to have three user types.
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
enum UserType {
Guest,
RegularUser,
Admin
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct User {
id: String,
first_name: Option<String>,
last_name: Option<String>,
user_type: UserType,
}
/// An admin is still a user
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
struct Admin(User);
Persisting Session Data
Right now we will store our sessions on a Hashmap but remember you can extend this to use a database such as Postgres.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
struct Sessions {
map: HashMap<String, User>,
}
Logging In
We are going to use actix-identity
to store our user_id. This will allow us to be able to retieve the user_id from identity and use it to get the User
object from session data.
#[post("/login")]
async fn login(login: web::Json<Login>, sessions: web::Data<RwLock<Sessions>>, identity: Identity) -> impl Responder {
let id = login.id.to_string();
let scope = &login.scope;
//let user = fetch_user(login).await // from db?
identity.remember(id.clone());
let user = User {
id: id.clone(),
last_name: Some(String::from("Doe")),
first_name: Some(String::from("John")),
user_type: scope.clone(),
};
sessions
.write()
.unwrap()
.map
.insert(id, user.clone());
info!("login user: {:?}", user);
HttpResponse::Ok().json(user)
}
Extractors
Up to now, our initial code could not run because both User
and Admin
dont implement FromRequest
Lets now write our User
extractor:
impl FromRequest for User {
type Config = ();
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<User, Error>>>>;
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
let fut = Identity::from_request(req, pl);
let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
if sessions.is_none() {
warn!("sessions is empty(none)!");
return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
}
let sessions = sessions.unwrap().clone();
Box::pin(async move {
if let Some(identity) = fut.await?.identity() {
if let Some(user) = sessions
.read()
.unwrap()
.map
.get(&identity)
.map(|x| x.clone())
{
return Ok(user);
}
};
Err(ErrorUnauthorized("unauthorized"))
})
}
}
This checks whether the identity exists and the id is found in our Session data.
We can do the same for Admin
impl FromRequest for Admin {
type Config = ();
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Admin, Error>>>>;
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
let fut = User::from_request(req, pl);
Box::pin(async move {
if let Some(user) = fut.await? {
if user.user_type != Scope::Admin {
return Err(ErrorUnauthorized("Not an Admin"));
}
return Ok(Admin(user));
}
Err(ErrorUnauthorized("unauthorized"))
})
}
}
And thats a wrap. We have been able to use extractors to verify a user is authenticated and matched their type.