rustapi-keyrust-warp

API key validation in warp (Rust)


I'm trying get started with warp and testing an api key validation. The following code works but it's not nice.

The validation function extracts the key from the header. After a successful validation the key is no longer used but "handle_request" function needs to have an input parameter for it.

Can you please advise on how to avoid that unwanted input parameter and cleaner approach to api key validation with warp?

Thanks in advance!

use std::convert::Infallible;
use std::error::Error;
use serde::{Deserialize, Serialize};
use warp::http::{Response, StatusCode};
use warp::{reject, Filter, Rejection, Reply};
//use futures::future;
// use headers::{Header, HeaderMapExt};
// use http::header::HeaderValue;
// use http::HeaderMap;

extern crate pretty_env_logger;
#[macro_use] extern crate log;

#[derive(Deserialize, Serialize)]
struct Params {
    key1: String,
    key2: u32,
}

#[derive(Debug)]
struct Unauthorized;

impl reject::Reject for Unauthorized {}

#[tokio::main]
async fn main() {
    pretty_env_logger::init();
    // get /exampel?key1=value&key2=42
    let route1 = warp::get().and(key_validation())
        .and(warp::query::<Params>())
        .and_then(handle_request);
       
    let routes = route1.recover(handle_rejection);
    warp::serve(routes)
        .run(([127, 0, 0, 1], 3030))
        .await;
}

async fn handle_request(api_key:String, params: Params) -> Result<impl warp::Reply, warp::Rejection> {
    Ok(Response::builder().body(format!("key1 = {}, key2 = {}", params.key1, params.key2)))
}

fn key_validation() -> impl Filter<Extract = (String,), Error = Rejection> + Copy {
    warp::header::<String>("x-api-key").and_then(|n: String| async move {
        if n == "test" {
            Ok(n)
        } else {
            Err(reject::custom(Unauthorized))
        }
    })
}

// JSON replies

/// An API error serializable to JSON.
#[derive(Serialize)]
struct ErrorMessage {
    code: u16,
    message: String,
}

// This function receives a `Rejection` and tries to return a custom
// value, otherwise simply passes the rejection along.
async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> {
    let code;
    let message;

    if err.is_not_found() {
        code = StatusCode::NOT_FOUND;
        message = "NOT_FOUND";
    } else if let Some(Unauthorized) = err.find() {
        code = StatusCode::UNAUTHORIZED;
        message = "Invalide API key";
    } else if let Some(_) = err.find::<warp::reject::MethodNotAllowed>() {
       // We can handle a specific error, here METHOD_NOT_ALLOWED,
        // and render it however we want
        code = StatusCode::METHOD_NOT_ALLOWED;
        message = "METHOD_NOT_ALLOWED";
    } else {
        // We should have expected this... Just log and say its a 500
        error!("unhandled rejection: {:?}", err);
        code = StatusCode::INTERNAL_SERVER_ERROR;
        message = "UNHANDLED_REJECTION";
    }

    let json = warp::reply::json(&ErrorMessage {
        code: code.as_u16(),
        message: message.into(),
    });

    Ok(warp::reply::with_status(json, code))
}

Update: When I try to avoid extracting some with the "key_validation" function I get this error:

error[E0271]: type mismatch resolving `<warp::filter::and_then::AndThen<impl warp::Filter+Copy, [closure@src/main.rs:44:50: 50:6]> as warp::filter::FilterBase>::Extract == ()`
  --> src/main.rs:43:24
   |
43 | fn key_validation() -> impl Filter<Extract = (), Error = Rejection> + Copy {
   |                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected tuple, found `()`
   |
   = note:  expected tuple `(_,)`
           found unit type `()`

To fix this I tried:

async fn handle_request(params: Params) -> Result<impl warp::Reply, warp::Rejection> {
    Ok(Response::builder().body(format!("key1 = {}, key2 = {}", params.key1, params.key2)))
}

fn key_validation() -> impl Filter<Extract = ((),), Error = Rejection> + Copy {
    warp::header::<String>("x-api-key").and_then(|n: String| async move {
        if n == "test" {
            Ok(())
        } else {
            Err(reject::custom(Unauthorized))
        }
    })
}

which results in this:

error[E0593]: function is expected to take 2 arguments, but it takes 1 argument
  --> src/main.rs:31:19
   |
31 |         .and_then(handle_request);
   |                   ^^^^^^^^^^^^^^ expected function that takes 2 arguments
...
39 | async fn handle_request(params: Params) -> Result<impl warp::Reply, warp::Rejection> {
   | ------------------------------------------------------------------------------------ takes 1 argument
   |
   = note: required because of the requirements on the impl of `warp::generic::Func<((), Params)>` for `fn(Params) -> impl Future {handle_request}`

these are the used dependencies:

[dependencies]
log = "0.4"
pretty_env_logger = "0.4"
tokio = { version = "1", features = ["full"] }
warp = "0.3"
serde = { version = "1.0", features = ["derive"] }
futures = { version = "0.3", default-features = false, features = ["alloc"] }


Solution

  • Just make your method to extract nothing:

    async fn handle_request(params: Params) -> Result<impl warp::Reply, warp::Rejection> {
        Ok(Response::builder().body(format!("key1 = {}, key2 = {}", params.key1, params.key2)))
    }
    
    
    fn key_validation() -> impl Filter<Extract = (), Error = Rejection> + Copy {
        warp::header::<String>("x-api-key").and_then(|n: String| async move {
            if n == "test" {
                Ok(())
            } else {
                Err(reject::custom(Unauthorized))
            }
        })
    }
    

    Probably you would need to discard the key_validation result value, use untuple_one:

    let route1 = warp::get()
            .and(key_validation())
            .untuple_one()
            .and(warp::query::<Params>())
            .and_then(handle_request);