How to set HMAC CSRF token, after the user logs in?

So I've been doing some research on web security, in which I came upon on this OWASP article on HMAC CSRF tokens. With that in mind, I've been trying to implement it using this middleware:

impl<S, B> Service<ServiceRequest> for CSRFTokenMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    dev::forward_ready!(service);

    fn call(&self, mut request: ServiceRequest) -> Self::Future {
        let mut res = self.service.call(request);
        
        Box::pin(async move {
            let res = res.await;
            set_csrf_token(res.unwrap().response_mut().head_mut(), &request);
            res.map(ServiceResponse::map_into_left_body)
        })        
    }
}

I'm not done yet, but it's supposed to set a token after the user successfully logs in. The issue is that self.service.call() takes ownership of the ServiceRequest but I need it for my set_csrf_token() function. Here's how that's like:

pub fn set_csrf_token(response: &mut ResponseHead, req: &ServiceRequest) -> Result<(), ()> {
    let csrf_token = generate_csrf_token(req);
    let cookie = Cookie::new("csrf", csrf_token);

    let val = HeaderValue::from_str(cookie.to_string().as_str()).unwrap();

    response.headers_mut().append(SET_COOKIE, val);

    Ok(())
}

And the corresponding, generate_csrf_token() function inside set_csrf_token() is:

pub fn generate_csrf_token(req: &ServiceRequest) -> String {
    let session_id = req.cookie("id").unwrap().value().to_string();

    let hmac_key_value = generate_hmac_key_value();
    let s_key = hmac::Key::new(hmac::HMAC_SHA256, hmac_key_value.as_ref());
    let random_value = Uuid::new_v4();

    let message = session_id.clone() + "!" + random_value.to_string().as_str();
    let hmac = hmac::sign(&s_key, message.as_bytes());

    let hmac_string = hex::encode(hmac.as_ref());
    let csrf_token = hmac_string + "." + message.as_str();

    csrf_token
}

I thought of making a POST request handler alongside my login function (which would be in the same resource), but that would set the token regardless of whether the user successfully logged in or not.

Would really appreciate any help. Thank you!

The problem is that in set_csrf_token you are both taking a reference to the request and also taking an exclusive reference to the response. You cannot do that in actix-web since, as you have figured out, by the time you call service.call you no longer have access to the request object.

The solution is as simple as splitting the logic in your functions: One that deals with the request, and one that deals with the response.

1 Like

the linearity of request is by design. when the service is called, the request is consumed.

if your middleware was doing work before the wrapped inner service, you are supposed to "transform" the request, and can use the request directly, just make sure you have a request value when you call the inner service;

on the other hand, if your middleware add logic after the inner service is called, you are supposed to do something with the response, you are not supposed to act on the request directly at this point.

basically, you have 2 choices:

  1. split your logic into two parts: "pre-call" (to handle the request) and "post-call" (to handle the response). for example, in the pre-call, extract the required information to do your work from the request (or even clone the entire request if that's what you need) and save it (and possibly do partial calculation), and use the saved the information (or partial result) in the "post-call" to calculate the final result

  2. in the actix-web case, although the request argument is consumed after the inner service is called, the information of the actual request is not gone! you still have access to them, just not directly: they are available at ServiceResponse:request():


EDIT:

keep in mind that, the request argument before the call, and the the request from the response after the call are not necessarily equal: the latter may have been transformed by the next layers of middleware (and/or the inner-most service itself). however, if your middleware cares about the response, the second choice should be the preferred one anyway (because that's the actual request that the response is generated from).

2 Likes

Thanks so much. To be honest, I never really understood what role service.call() played but now I get it. I'll work on it and see how it goes. Really appreciated.

I tried your recommendation but I have to handle the response as I wouldn't have access to the session cookie until then, as @nerditation mentioned. Though I never thought of splitting the logic of my functions before, so thanks for that advice

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.