Actix web error during authorization

I'm creating a bearer token for testing purposes with an endpoint but I get an error during the authorization and I think I miss something in the endpoint function.

Error in response: AuthenticationError { challenge: Bearer { scope: None, realm: None, error: None, error_description: None, error_uri: None }, status_code: 401 }
// This is the endpoint I want to test
#[get("/blacklight/{url}")]
pub async fn blacklight(
    path: web::Path<String>, 
    claims: Option<web::ReqData<Claims>>
) -> impl Responder {
    let url: String = path.into_inner();

    match url_check::is_url(url.as_str()) {
        true => {
            if let Some(claims) = claims {
                publish_service::publisher(claims.sub.clone(), &url);
            }

            HttpResponse::Ok().json("Ok")
        },
        false => HttpResponse::BadRequest().body("ProtocolError: Cannot navigate to invalid URL")
    }
}
// This is the endpoint to create a test token
#[post("/createJWT")]
pub async fn encoder(claims: web::Json<Claims>) -> impl Responder {
    let env: String = std::env::var("PRIV_KEY")
        .expect("PRIV_KEY must be set.");
    let read_from_file: String = std::fs::read_to_string(env).unwrap();
    let key: &str = read_from_file.as_str();
    let header: Header = Header::new(jsonwebtoken::Algorithm::ES256);

    let token: String = encode(
        &header, 
        &claims, 
        &EncodingKey::from_ec_pem(&key.as_bytes()).unwrap()
    ).unwrap();

    info!("Encode new token...\n Token: {}", token.clone());

    HttpResponse::Created().json(token.clone())
}
// This is my validation function
pub fn validate_token(token: &str) -> Result<jsonwebtoken::TokenData<Claims>, HttpResponse> {
    let val: Validation = Validation::new(Algorithm::HS256);
    let env: String = std::env::var("PUB_KEY")
        .expect("PUB_KEY must be set.");
    let pub_key: &[u8] = env.as_bytes();

    let unauth: &str = "The access token provided is expired, revoked, malformed, or invalid";
    let bad_req: &str = "The request has a invalid or missing parameter";
    let bad_gate: &str = "The request can't be handled";

    match decode::<Claims>(&token, &DecodingKey::from_ec_pem(pub_key).unwrap(), &val) {
        Ok(c) => Ok(c),
        Err(err) => match *err.kind() {
            ErrorKind::InvalidToken => Err(HttpResponse::Unauthorized().body(unauth)),
            ErrorKind::InvalidIssuer => Err(HttpResponse::BadRequest().body(bad_req)),
            _ => Err(HttpResponse::BadGateway().body(bad_gate)),
        },
    }
}
async fn validator(
    req: ServiceRequest,
    credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
    let config: bearer::Config = req.app_data::<bearer::Config>()
            .cloned()
            .unwrap_or_default()
            .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");

    match auth_service::validate_token(credentials.token()) {
        Ok(res) => {
            info!("Valid request");
            req
                .extensions_mut()
                .insert(res.claims);
            Ok(req)
        },
        Err(_) => Err((AuthenticationError::from(config).into(), req))
    }
}

I'm pretty sure you are running into the error here:

ErrorKind::InvalidToken => Err(HttpResponse::Unauthorized().body(unauth)),

in your validate_token function.

Not sure, but you decode and validate your token with HMAC:

let val: Validation = Validation::new(Algorithm::HS256);
//                                               ^^^^^

while you encode your token with ECDSA:

let header: Header = Header::new(jsonwebtoken::Algorithm::ES256);
//                                                        ^^^^^

I wonder if that's causing you the error?

I wonder if that's causing you the error?

I changed it and it didn't solve the error.

I'm pretty sure you are running into the error here

Yes and I'm not sure if I'm missing something that I have to implement.

Do you know how I can change this error message for better details? Because all values of this JSON are None.

Error in response: AuthenticationError { 
challenge: Bearer { 
    scope: None, 
    realm: None, 
    error: None, 
    error_description: None, 
    error_uri: None 
}, status_code: 401 
}

Without more information it's hard to guess why decoding your token doesn't work. Could you log the error from decode and share it here, please?

It didn't even jump into the decode function. I don't get the log message.

It's because you are throwing an AuthenticationError::from(config).into() here. You could try and return the jsonwebtoken::errors::Error directly from validate_token and use that as the error from your validator middleware function.

I just realized that the BearerAuth extractor is probably failing, which is why we never even enter the body of validator. I guess the authorization header is malformed, it should look like this:

Authorization: Bearer <your_token>

I use this format. I'm adding it to a Swagger param which I declared like this:

#[utoipa::path(
    context_path = "/tools",
    tag = "tools",
    params(
        ("url", 
            description = "URL that should be scanned for fingerprints.", 
            example = "https://example.com"
        ),
        ("Authorization" = String, Header, description = "JWT")
    ),
    responses(
        (status = 200, description = "JSON output of the blacklight collector.", body = String),
        (status = 400, description = "Wrong URL.", body = String)
    )
)]

But I can't figured out if it adds the param to the HTTP header.
It seems that the Authorization header can't be found because it didn't jump into:

Err(err) => {
   error!("Validate Token Error: {:#?}", err);
   Err((AuthenticationError::from(config).into(), req))
}

Could you log the actual request and see what the header looks like, please? I'm pretty sure your authorization header value is malformed.

Do I need to add the header as an argument in my endpoint function?

No, logging req should suffice I think. For now, I'd try something like this to debug quickly:

async fn validator(
    req: ServiceRequest,
    // credentials: BearerAuth,
) -> Result<ServiceRequest, (Error, ServiceRequest)> {
    let config: bearer::Config = req.app_data::<bearer::Config>()
            .cloned()
            .unwrap_or_default()
            .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
    

    info!(?req);

    Err((AuthenticationError::from(config).into(), req))

    /*
    match auth_service::validate_token(credentials.token()) {
        Ok(res) => {
            info!("Valid request");
            req
                .extensions_mut()
                .insert(res.claims);
            Ok(req)
        },
        Err(_) => Err((AuthenticationError::from(config).into(), req))
    }
    */
}

Just be sure to comment out credentials, as I believe the error is happening while actix tries to extract them from the request.

If I comment out credentials I get:

function is expected to take 2 arguments, but it takes 1 argument
expected function that takes 2 arguments

Ah yes, we must extract something in order for the HttpAuthentication middleware to be happy. Just extract the body instead:

async fn validator(
    req: ServiceRequest,
    // credentials: BearerAuth,
    _body: actix_web::web::Bytes,
)

You'll have to change your constructor from HttpAuthentication::bearer(validator) to HttpAuthentication::wrap_fn(validator) as well.

That gave me:

type mismatch in function arguments
expected function signature `fn(ServiceRequest, BearerAuth) -> _`
   found function signature `fn(ServiceRequest, actix_web::web::Bytes) -> _`

Did you do that as well?

HttpAuthentication::wrap_fn(validator) is unknown. You mean HttpAuthentication::with_fn(validator)?

My bad, yes

info!("{:#?}", req);
Err((AuthenticationError::from(config).into(), req))

The log doesn't show anything.

Finally:

isumis                | 2023-11-09T10:51:27.332Z DEBUG [isumis::server] 
isumis                | ServiceRequest HTTP/1.1 GET:/tools/blacklight/https%3A%2F%2Fexample.com
isumis                |   headers:
isumis                |     "accept-encoding": "gzip, deflate, br"
isumis                |     "sec-fetch-dest": "empty"
isumis                |     "dnt": "1"
isumis                |     "sec-fetch-mode": "cors"
isumis                |     "connection": "keep-alive"
isumis                |     "sec-fetch-site": "same-origin"
isumis                |     "user-agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0"
isumis                |     "accept": "text/plain"
isumis                |     "host": "127.0.0.1:8088"
isumis                |     "accept-language": "en-US,en;q=0.5"
isumis                |     "referer": "http://127.0.0.1:8088/api/"

As I thought, the header is missing.

2 Likes

Finnally, I got the error message which causes the error.

2023-11-10T06:59:34.648Z DEBUG [isumis::service::auth_service] Missing required claim: exp
2023-11-10T06:59:34.648Z DEBUG [isumis::server] HttpResponse:
HttpResponse {
    error: None,
    res:
    Response HTTP/1.1 502 Bad Gateway
    headers:
    body: Sized(27)
}

I'm a bit confused because I pass the claims as a struct to the encode().

#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct Claims {
    #[schema(example = "admin")]
    aud: String,
    #[schema(example = "1234")]
    pub sub: String,
    #[schema(example = "1000000")]
    exp: String
}
let token: String = encode(
   &Header::new(jsonwebtoken::Algorithm::RS256), 
   &claims, 
   &EncodingKey::from_rsa_pem(&key.as_bytes()).unwrap()
).unwrap();