Axum Middleware: Pass Json and State

Hi,

I need to get "state" and request body as a json. Normally for routing handler I can do this:

async fn handler(
    State(app_state): State<AppState>,
    Json(json): Json<serde_json::Value>,
)

but for middleware I can't do this:

async fn middleware(
    State(app_state): State<AppState>,
    Json(json): Json<serde_json::Value>,
    mut request: Request,
    next: Next,
)

in official doc I couldn't find how can I pass json to middleware but I found how can I pass state. Still couldn't make it work:

async fn route(State(state): State<AppState>) -> Router {
   Router::new()
      .route("/", get(something))
      .route_layer(
         axum::middleware::from_fn_with_state(
            state.clone(),  
            middleware
   )).with_state(state);
}

In the end I need body as json and state in middleware. What do you suggest?

The JSON extractor must be the last extractor, let alone it will consume the request body.

Are you sure this is what you want in a middleware?

I will need json body in router handler too so if it consumes I think I may need to inject it again. If there is other way I would use it. I need to check function body because I have to check if user modifies it's own property or others. I mean I'm trying to implement is_self mechanism if you familiar with other frameworks like Django.

By the way putting Json at the end didn't solve the problem. I don't know why but even this (without json only appstate) is not working:

async fn pass(
    State(app_state): State<AppState>,
    mut request: Request,
    next: Next,
) -> impl IntoResponse {}

async fn route(app_state: AppState) {
   let router = Router::new()
        .route("/", get(something))
        .route_layer(axum::middleware::from_fn_with_state(
            app_state.clone(),
            pass,
        ))
        .with_state(app_state);
}

I get trait bound error

Isn't that better implemented as an extractor, rather than a middleware?

That's weird, I just tested your middleware locally and it works as expected. Try annotating your middleware function with the #[debug_middleware] macro and also post the full compilation error.

A quick search for is_self django didn't return any meaningful results. Perhaps you could also post a link to what exactly you're after.

Afaik if I use extractor, I may need to modify every router handlers to check if user's modifying it's own property. I mean in handler i need to check

if user_from_jwt_token == user_in_json_body {
some operations
} else {
StatusCode:FORBIDDEN
}

I thought if I do it with middleware I can check as much as routing with same middleware instead of modifying every router handler.

Router::new()
.route("a", post(a)).middleware(allow_self)
.route("b", update(b)).middleware(allow_self)

These thoughts may be wrong and maybe I understand extractor in a wrong way. If it is please correct me.

I think I'm missing something.

Commented middleware works but other gives that error.

I tried Result<impl IntoResponse, StatusCode> instead of impl IntroResponse too.

sorry for being so specific when I said is_self in Django, i meant: Is request object belongs to the same person who sends request. It does it with object permission mechanism and basically checks if user_id in jwt token same with user_id in request object body. It tries to match user.

Alright I activated debug_middleware

I was thinking of a custom extractor UserJson that implements FromRequest and that you use instead of the Json extractor as argument to your handlers that require the validation of the user data in the body against the JWT.

This seems an uncommon check to me. I usually have my user endpoints behind a /user/{id}, that way I can check my JWT against the id in my path, without having to look at the body of my request.

You are not showing the implementation for user_extraction, but given the error you are getting I'm betting you are consuming the request body. That's why I warned you against following this path.

I never done this, can you give me a little example about it?

I have different kind of endpoints. Sometimes it's like your example especially in get requests but for post, update request I need to check with body.

That's a code smell. You shouldn't design your API in such a way that one user might modify another user's data.

1 Like

Sorry I'm trying to exclude boilerplate as much as possible

async fn user_extraction(request: &Request, database_connection: &Pool<Postgres>) -> Option<User> {
    if let Some(authorization_header) = request.headers().get(http::header::AUTHORIZATION) {
        if let Ok(authorization_header) = authorization_header.to_str() {
            if let Some((bearer, authorization_header)) = authorization_header.split_once(' ') {
                if bearer == "bearer" {
                    if let Ok(claims) =
                        TokenMeta::verify_token(&authorization_header.to_string()).await
                    {
                        return User::read(&claims.custom, database_connection).await.ok();
                    }
                }
            }
        }
    }
    None
}

I'm creating a forum. Admin must modify user data in some cases.

Isn't that better represented as an additional claim in the JWT?

The output of #[debug_middleware] contained more information that was useful to understand the issue. In the future please post the full output so that we can better help you.

The issue is that you are passing a reference to an async function (You pass a reference to Request in user_extraction), but this can only work if their referent is Sync (which is not the case for Request).

Unfortunately this means that you'll have to extract the user within the middleware function.

1 Like

Thanks a lot, I fixed problem with sending request and returning it back instead of only sharing reference.

Now we can pass to another part. I need to parse body as a Json but I cant use both
Request and Json extractor together. (Thanks a lot btw, #[debug_middleware] gives perfect feedback)

Can't have two extractors that consume the request body. `Json<_>` and `Request<_>` both do that.

So I think I have to do it manually by checking request body but how can I deserialize it as json and check specific field?

Sorry, I didn't understand what is in your mind.

In OpenID Connect you can add custom claims to your JWT. That could be admin: true, for example. If you are using some other protocol, I'm sure there is a similar way for you to add application-specific metadata to your JWT. That way you'd know that a user is an admin and that they can change the user data for other users. Then we are back at

1 Like

I got it but even though I fix admin check. I will need to check body somehow because of my strategy of endpoint access but thanks anyway.

I don't know if it's going to work but I'm extracting json and "kind of" cloning body.

let (parts, body) = request.into_parts();
let bytes = to_bytes(body, usize::MAX).await?;
let json: serde_json::Value = serde_json::from_slice(&bytes)?;

let body = Body::from(json.to_string());
let request = Request::from_parts(parts, body);