Auto-redirect in REST client tools

While preparing the sample project for my blog post about JWT authentication, I spent too much time troubleshooting an issue with failed authentication of calls from .http files in Visual Studio. It turned out to be caused by how Visual Studio handles redirects. I decided to check how Bruno and Postman behave in this scenario.

Newly created ASP.NET Core Web API projects have HTTP to HTTPS redirection enabled by default:

app.UseHttpsRedirection();

This means that any request to the HTTP URL will receive a 307 Temporary Redirect response with the equivalent HTTPS URL:

GET http://localhost:5194/weatherforecast/

HTTP/1.1 307
Location: https://localhost:7071/weatherforecast/

If the said URL requires authentication, requests to either URL without a valid Authorization header will be rejected with a 401 Unauthorized response.

However, when I sent a request with a valid Authorization header to the HTTP URL from an .http file in Visual Studio, I got a 401 Unauthorized response without a clear indication that this wasn't even a response to the original request to the HTTP URL:

GET http://localhost:5194/weatherforecast/
Accept: application/json
Authorization: Bearer {{JwtToken}}

401 response in Headers tab of a Visual Studio .http file

The only information hinting to that was on the Request tab:

Request tab of a Visual Studio .http file

Here, I could see that the request was actually made to the HTTPS URL and that it didn't include any Authorization header which explained the 401 Unathorized response. However, there was no record of the 307 Temporary Redirect response to the original HTTP request and hence no real explanation why the request made was different from the one in the .http file.

Still, it was enough to explain why the request failed to authorize. Once I made the request with the Authorization header directly to the HTTPS URL, it succeeded as expected:

GET https://localhost:7071/weatherforecast/
Accept: application/json
Authorization: Bearer {{JwtToken}}

200 response in Headers tab of a Visual Studio .http file

Learning that .http files in Visual Studio behaved like this made me curious how other REST client tools handle such a scenario.

Bruno (v1.19.0) handles it even worse: when it receives a 307 Temporary Redirect response from the HTTP URL, it also automatically sends the request to the HTTPS URL without the Authorization header. However, there is no way to see from inside Bruno that this has happened. The Timeline pane makes it appear as if the 401 Unauthorized response belongs to the original HTTP request not the HTTPS request after the redirect.

Timeline pane in Bruno

Postman handles it much better, though:

  • In the Console pane, there is a clear record that two requests were made: one to the original HTTP URL with the 307 Temporary Redirect response, and another to the HTTPS URL with a 200 OK response.
  • The request to the redirect HTTPS URL also includes the original Authorization header, therefore the request succeeds instead of failing with 401 Unauthorized.

Console pane in Postman

Since the REST clients (except for Postman) don't provide full information about the requests being made, I wanted to improve the logging in my ASP.NET Core Web API project to confirm my assumptions about what was happening. I found two ways to do that.

The simpler one is to enable HTTP logging:

  • add the required services to dependency injection:
    builder.Services.AddHttpLogging(options =>
    {
        options.LoggingFields = HttpLoggingFields.All;
    });
    
  • add the logging middleware to the pipeline:
    app.UseHttpLogging();
    
  • increase the HTTP logging level to at least Information:
    {
      "Logging": {
        "LogLevel": {
          "Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
        }
      }
    }
    

With this configuration in place, the logs will contain full information about both requests being made and responses to them:

Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware: Information: Request:
Protocol: HTTP/1.1
Method: GET
Scheme: http
PathBase:
Path: /weatherforecast/
Accept: application/json, text/plain, */*
Connection: close
Host: localhost:5194
User-Agent: axios/1.7.2
Accept-Encoding: gzip, compress, deflate, br
Authorization: [Redacted]
request-start-time: [Redacted]
Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware: Information: Response:
StatusCode: 307
Location: https://localhost:7071/weatherforecast/
Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware: Information: Duration: 3.2122ms
Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware: Information: Request:
Protocol: HTTP/1.1
Method: GET
Scheme: https
PathBase:
Path: /weatherforecast/
Accept: application/json, text/plain, */*
Connection: keep-alive
Host: localhost:7071
User-Agent: axios/1.7.2
Accept-Encoding: gzip, compress, deflate, br
request-start-time: [Redacted]
Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware: Information: Response:
StatusCode: 401
WWW-Authenticate: [Redacted]
Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware: Information: Duration: 8.9213ms

It can also be clearly seen that the HTTPS request didn't include the Authorization header.

If you want to get more information about the JWT validation process, you can register handlers for selected events exposed by the JWT authentication code. While you could assign handlers directly to the Events property of the JwtBearerOptions class, I don't think there's a way to inject a logger instance there. If you want to do that to properly implement logging, you need to derive your own class from the JwrBearerEvents base class:

public class LoggingJwtBearerEvents(ILogger<LoggingJwtBearerEvents> logger)
    : JwtBearerEvents
{
    public override Task MessageReceived(MessageReceivedContext context)
    {
        logger.LogInformation(
            "MessageReceived: {Scheme}://{Host}",
            context.Request.Scheme,
            context.Request.Host
        );
        return base.MessageReceived(context);
    }

    public override Task TokenValidated(TokenValidatedContext context)
    {
        logger.LogInformation(
            "TokenValidated: {Scheme}://{Host}",
            context.Request.Scheme,
            context.Request.Host
        );
        return base.TokenValidated(context);
    }

    public override Task Challenge(JwtBearerChallengeContext context)
    {
        logger.LogInformation(
            "Challenge, {Scheme}://{Host}",
            context.Request.Scheme,
            context.Request.Host
        );
        return base.Challenge(context);
    }
}

You can then assign this class to the EventsType property of the JwtBearerOptions class:

options.EventsType = typeof(LoggingJwtBearerEvents);

And don't forget to register your class with dependency injection so that it can be successfully instantiated:

builder.Services.AddScoped<LoggingJwtBearerEvents>();

In the code above I only logged the information about the hosts the requests were sent to so that I could differentiate between the HTTP and HTTPS requests:

WebApiOAuth.LoggingJwtBearerEvents: Information: MessageReceived: http://localhost:5194
WebApiOAuth.LoggingJwtBearerEvents: Information: TokenValidated: http://localhost:5194
WebApiOAuth.LoggingJwtBearerEvents: Information: MessageReceived: https://localhost:7071
WebApiOAuth.LoggingJwtBearerEvents: Information: Challenge, https://localhost:7071

But since you have access to full context in the event handlers, you can log as many details as you want.

You can find full source code for a working sample project with all this logging in place in my GitHub repository. I also included the request files I used with my REST client tools of choice: .http files in Visual Studio, Bruno and Postman.

I learned the hard way that the REST client tools we commonly use don't all provide full information about the requests they are making. Understanding their behavior makes it easier to troubleshoot issues you might encounter during development. Adding more logging to the server application is also a good way to get a clearer picture of what is happening.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License