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}}
The only information hinting to that was on the Request tab:
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}}
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.
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 a200 OK
response. - The request to the redirect HTTPS URL also includes the original
Authorization
header, therefore the request succeeds instead of failing with401 Unauthorized
.
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.