Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make protected routes in actix-web

I need to verify if the user has permission for some routes. I have made 3 "scopes" (guest, auth-user, admin) and now I don't know how to check if the user has access to these routes.

I'm trying to implement auth-middleware and this middleware should check if the user has the correct cookie or token. (I'm able to print out a cookie from request header), but I have no idea how to import, use actix_identity, and have access to id parameter inside this middleware.

I believe that my problem isn't only regarding Actix-identity, but I'm not able to pass parameters inside middleware.

    #[actix_rt::main]
    async fn main() -> std::io::Result<()> {

        let cookie_key = conf.server.key;
    
        // Register http routes
        let mut server = HttpServer::new(move || {
            App::new()
                // Enable logger
                .wrap(Logger::default())
                .wrap(IdentityService::new(
                    CookieIdentityPolicy::new(cookie_key.as_bytes())
                        .name("auth-cookie")
                        .path("/")
                        .secure(false),
                ))
                //limit the maximum amount of data that server will accept
                .data(web::JsonConfig::default().limit(4096))
                //normal routes
                .service(web::resource("/").route(web::get().to(status)))
                // .configure(routes)
                .service(
                    web::scope("/api")
                        // guest endpoints
                        .service(web::resource("/user_login").route(web::post().to(login)))
                        .service(web::resource("/user_logout").route(web::post().to(logout)))
                        // admin endpoints
                        .service(
                            web::scope("/admin")
                                // .wrap(AdminAuthMiddleware)
                                .service(
                                    web::resource("/create_admin").route(web::post().to(create_admin)),
                                )
                                .service(
                                    web::resource("/delete_admin/{username}/{_:/?}")
                                        .route(web::delete().to(delete_admin)),
                                ),
                        )
                        //user auth routes
                        .service(
                            web::scope("/auth")
                                // .wrap(UserAuthMiddleware)
                                .service(web::resource("/get_user").route(web::get().to(get_user))),
                        ),
                )
        });
    
        // Enables us to hot reload the server
        let mut listenfd = ListenFd::from_env();
        server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() {
            server.listen(l)?
        } else {
            server.bind(ip)?
        };
    
        server.run().await

resources that I have tried:

  1. Creating authentication middleware for Actix API https://www.jamesbaum.co.uk/blether/creating-authentication-middleware-actix-rust-react/

  2. Actix-web token validation in middleware https://users.rust-lang.org/t/actix-web-token-validation-in-middleware/38205

  3. Actix middleware examples https://github.com/actix/examples/tree/master/middleware

Maybe I think completely wrong and auth-middleware isn't the best solution for my problem. I hope that you can help me create "protected routes"

like image 646
Karol Avatar asked Jun 08 '20 18:06

Karol


Video Answer


4 Answers

Try extractors instead

Trying to implement this pattern in Actix 3 I banged my head for awhile trying to use middleware, basically making a guard and then figuring out how to pass data from the middleware into the handler. It was painful and eventually I realized that I was working against Actix rather than with it.

Finally I learned out that the way to get information to a handler is to create a struct (AuthedUser, perhaps?) and implement the FromRequest trait on that struct.

Then every handler that asks for an AuthedUser in the function signature will be auth gated and if the user is logged in will have any user information you attach to AuthedUser in the FromRequest::from_request method.

Actix refers to these structs that implement FromRequest as extractors. It's a bit of magic that could use more attention in the guide.

like image 97
Doug Bradshaw Avatar answered Oct 23 '22 06:10

Doug Bradshaw


The following does not use middleware(a little bit more work is needed) but it solves the problem with the bear minimum and seems to be the approach suggested in documentation:

#[macro_use]
extern crate actix_web;
use actix::prelude::*;
use actix_identity::{CookieIdentityPolicy, Identity, IdentityService};
use actix_web::{
    dev::Payload, error::ErrorUnauthorized, web, App, Error, FromRequest, HttpRequest,
    HttpResponse, HttpServer, Responder,
};
use log::{info, warn};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, pin::Pin, sync::RwLock};

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
struct Sessions {
    map: HashMap<String, User>,
}

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct Login {
    id: String,
    username: String,
    scope: Scope,
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
enum Scope {
    Guest,
    User,
    Admin,
}

impl Default for Scope {
    fn default() -> Self {
        Scope::Guest
    }
}

#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[serde(rename_all = "camelCase")]
struct User {
    id: String,
    first_name: Option<String>,
    last_name: Option<String>,
    authorities: Scope,
}

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"))
        })
    }
}

#[get("/admin")]
async fn admin(user: User) -> impl Responder {
    if user.authorities != Scope::Admin {
        return HttpResponse::Unauthorized().finish();
    }
    HttpResponse::Ok().body("You are an admin")
}

#[get("/account")]
async fn account(user: User) -> impl Responder {
    web::Json(user)
}

#[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")),
        authorities: scope.clone(),
    };
    sessions.write().unwrap().map.insert(id, user.clone());
    info!("login user: {:?}", user);
    HttpResponse::Ok().json(user)
}

#[post("/logout")]
async fn logout(sessions: web::Data<RwLock<Sessions>>, identity: Identity) -> impl Responder {
    if let Some(id) = identity.identity() {
        identity.forget();
        if let Some(user) = sessions.write().unwrap().map.remove(&id) {
            warn!("logout user: {:?}", user);
        }
    }
    HttpResponse::Unauthorized().finish()
}

#[actix_rt::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();

    let sessions = web::Data::new(RwLock::new(Sessions {
        map: HashMap::new(),
    }));

    HttpServer::new(move || {
        App::new()
            .app_data(sessions.clone())
            .wrap(IdentityService::new(
                CookieIdentityPolicy::new(&[0; 32])
                    .name("test")
                    .secure(false),
            ))
            .service(account)
            .service(login)
            .service(logout)
            .service(admin)
    })
    .bind("127.0.0.1:8088")?
    .run()
    .await
}

You can clone and run it here: https://github.com/geofmureithi/actix-acl-example

like image 39
Njuguna Mureithi Avatar answered Oct 23 '22 06:10

Njuguna Mureithi


I think actix-web grants crate is perfect for you. It allows you to check authorization using Guard, or a procedural macro (see examples on github). It also integrates nicely with existing authorization middleware (like actix-web-httpauth).

A couple of examples for clarity:

  • proc-macro way
#[get("/secure")]
#[has_permissions("ROLE_ADMIN")]
async fn macro_secured() -> HttpResponse {
    HttpResponse::Ok().body("ADMIN_RESPONSE")
}
  • Guard way
App::new()
    .wrap(GrantsMiddleware::with_extractor(extract))
    .service(web::resource("/admin")
            .to(|| async { HttpResponse::Ok().finish() })
            .guard(PermissionGuard::new("ROLE_ADMIN".to_string())))

And you can also take a look towards actix-casbin-auth (implementation of casbin integrated into actix)

like image 45
DDtKey Avatar answered Oct 23 '22 04:10

DDtKey


Well this is in fact quite difficult to achieve in the newest actix-web version 3.0. What I did was copy the CookieIdentityPolicy middleware from the actix-web 1.0 version and modified it to my liking. However this is not plug & play code. Here and here is my version of it. Generally I would avoid actix-web, getting a thread / actor to spawn in the background and having it perform HTTP Requests are a nightmare. Then trying to share the results with handlers even more so.

like image 21
Qubasa Avatar answered Oct 23 '22 06:10

Qubasa