ASP Core API - Custom Unauthorized body

9.7k Views Asked by At

I'm developing ASP Core Web API using dotnet core v3.1.

I'm using JWT tokens for authentication. And for authorization I use the [Authorize] attribute.

How can I create my own response if the user is not logged in (while trying to access the action marked with the [Authorize] attribute) or the user's token is not authenticated.

I came across a solution using a custom authorization attribute inherited from the default one. And in this example, the HandleUnauthorizedRequest method is overridden. But I don't see such a method inside the AuthorizeAttribute class.

Is there a way to create custom unauthorized responses with http body?

3

There are 3 best solutions below

5
On BEST ANSWER

Since you are using JWT bearer authentication, one way to override the default Challenge logic (which executes to handle 401 Unauthorized concerns) is to hook a handler to the JwtBearerEvents.OnChallenge callback in Startup.ConfigureServices:

services.AddAuthentication().AddJwtBearer(options =>
{
    // Other configs...
    options.Events = new JwtBearerEvents
    {
        OnChallenge = async context =>
        {
            // Call this to skip the default logic and avoid using the default response
            context.HandleResponse();

            // Write to the response in any way you wish
            context.Response.StatusCode = 401;
            context.Response.Headers.Append("my-custom-header", "custom-value");
            await context.Response.WriteAsync("You are not authorized! (or some other custom message)");
        }
    };
});

This will override the default challenge logic in JwtBearerHandler.HandleChallengeAsync, which you can find here for reference purposes.

The default logic does not write any content to response (it only sets the status code and set some headers). So to keep using the default logic and add content on top of it, you can use something like this:

options.Events = new JwtBearerEvents
{
    OnChallenge = context =>
    {
        context.Response.OnStarting(async () =>
        {
            // Write to the response in any way you wish
            await context.Response.WriteAsync("You are not authorized! (or some other custom message)");
        });

        return Task.CompletedTask;
    }
};
0
On

For .net core 5 web api project with jwt authentication use this middleware in Configure method of Startup.cs for show ErrorDto in Swagger:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseSwagger();
        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "LoginService v1"));
    }

    app.ConfigureExceptionHandler();
    app.UseHttpsRedirection();
    app.UseRouting();
        
    // Unauthorized (401) MiddleWare
    app.Use(async (context, next) =>
    {
        await next();
     
        if (context.Response.StatusCode == (int)HttpStatusCode.Unauthorized) // 401
        {
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(new ErrorDto()
            {
                StatusCode = 401,
                Message = "Token is not valid"
            }.ToString());
        }
    });

    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
}

ErrorDto :

public class ErrorDto
{
    public int StatusCode { get; set; }
    public string Message { get; set; }
    
    public override string ToString()
    {
        return JsonSerializer.Serialize(this);
    }
}
2
On

This is what I came up with for responding with the same ProblemDetails you would get from returning Unauthorized() in an ApiController:

.AddJwtBearer(options =>
{
    // Other configs...
    options.Events = new JwtBearerEvents
    {
        OnChallenge = async context =>
        {
            // Call this to skip the default logic and avoid using the default response
            context.HandleResponse();

            var httpContext = context.HttpContext;
            var statusCode = StatusCodes.Status401Unauthorized;

            var routeData = httpContext.GetRouteData();
            var actionContext = new ActionContext(httpContext, routeData, new ActionDescriptor());

            var factory = httpContext.RequestServices.GetRequiredService<ProblemDetailsFactory>();
            var problemDetails = factory.CreateProblemDetails(httpContext, statusCode);

            var result = new ObjectResult(problemDetails) { StatusCode = statusCode };
            await result.ExecuteResultAsync(actionContext);
        }
    };
});