13
votes

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"

5

5 Answers

7
votes

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.

5
votes

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

2
votes

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)

1
votes

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.

1
votes

middleware doesn't look very friendly with all the generics and internal types it defines, but it is a simple struct that wrap the next service to be called. What is the next service is determined by the chain call when you create your App or define your routes. You use a generic S in your middleware which will be monomorphized at compile time so you don't have to care about which concrete type the middleware will protect.

The following middleware use a simple config passed to your App with .data() to check if the 'token' header contains the same magic value. It either go through the next service or return a not authorized error (futures).

use crate::config::Config;

use actix_service::{Service, Transform};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    error::ErrorUnauthorized,
    web::Data,
    Error,
};
use futures::future::{err, ok, Either, Ready};
use std::task::{Context, Poll};

pub struct TokenAuth;

impl<S, B> Transform<S> for TokenAuth
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = TokenAuthMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(TokenAuthMiddleware { service })
    }
}

pub struct TokenAuthMiddleware<S> {
    service: S,
}

impl<S, B> Service for TokenAuthMiddleware<S>
where
    S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
{    type Request = ServiceRequest;
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Either<S::Future, Ready<Result<Self::Response, Self::Error>>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        self.service.poll_ready(cx)
    }

    fn call(&mut self, req: ServiceRequest) -> Self::Future {
        if let Some(token) = req
            .headers()
            .get("token")
            .and_then(|token| token.to_str().ok())
        {
            if let Some(config) = req.app_data::<Data<Config>>() {
                if token == config.token {
                    return Either::Left(self.service.call(req));
                }
            }
        }
        Either::Right(err(ErrorUnauthorized("not authorized")))
    }
}

to protect your functions is then as simple as

#[post("/upload", wrap="TokenAuth")]
async fn upload(mut payload: Multipart) -> Result<HttpResponse, Error> {
}

Note that you need actix_service 1.x for this to compile. actix_service 2 remove the request internal type to make it generic and I couldn't make it work with the wrap="" syntax