15

I've derived OAuthAuthorizationServerProvider in order to validate both clients and resource owners.

When I validate resource owners I find their credentials aren't valid, I call context.Rejected(), and HTTP response comes with HTTP/400 Bad Request status code while I would expect HTTP/401 Unauthorized.

How can I customize OAuthAuthorizationServerProvider's response HTTP status codes?

Matías Fidemraizer
  • 63,804
  • 18
  • 124
  • 206

2 Answers2

16

This is how we override the OwinMiddleware...first we created our own middleware on top of Owin...I think we had similar issue as you did.

First need to create a constant:

public class Constants
{
    public const string OwinChallengeFlag = "X-Challenge";
}

And we override the OwinMiddleware

public class AuthenticationMiddleware : OwinMiddleware
{
    public AuthenticationMiddleware(OwinMiddleware next) : base(next) { }

    public override async Task Invoke(IOwinContext context)
    {
        await Next.Invoke(context);

        if (context.Response.StatusCode == 400 && context.Response.Headers.ContainsKey(Constants.OwinChallengeFlag))
        {
            var headerValues = context.Response.Headers.GetValues(Constants.OwinChallengeFlag);
            context.Response.StatusCode = Convert.ToInt16(headerValues.FirstOrDefault());
            context.Response.Headers.Remove(Constants.OwinChallengeFlag);
        }

    }
}

In the startup.Auth file, we allowed the overrid of the Invoke Owin Commands

public void ConfigureAuth(IAppBuilder app)
    ....
        app.Use<AuthenticationMiddleware>(); //Allows override of Invoke OWIN commands
    ....

    }

And in the ApplicationOAuthProvider, we modified the GrantResourceOwnerCredentials.

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        using (UserManager<IdentityUser> userManager = _userManagerFactory())
        {
            IdentityUser user = await userManager.FindAsync(context.UserName, context.Password);

            if (user == null)
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
                context.Response.Headers.Add(Constants.OwinChallengeFlag, new[] { ((int)HttpStatusCode.Unauthorized).ToString() }); //Little trick to get this to throw 401, refer to AuthenticationMiddleware for more
                //return;
            }
            ....
Greg P
  • 772
  • 2
  • 10
  • 23
  • I'll try this tomorrow and I'll confirm you if it worked for me!! Thank you in advance ;) – Matías Fidemraizer May 13 '15 at 19:26
  • Hey, I've trying your approach and I've finding issues. I guess `STTIAuthenticationMiddleware` is `AuthenticationMiddleware`, and my main issue is that custom middleware is never called during my pipeline. I've configured my middleware using `app.Use()` and `app.Use(System.Type)`. What's wrong? – Matías Fidemraizer May 14 '15 at 09:39
  • Yes, on the first part, those are the same. On the other part, did you verify that the startup.auth is calling ConfigureAuth and setting it? – Greg P May 14 '15 at 12:24
  • 1
    Yeah, absolutely. Think authentication is already working. I just want to change 400 to 401 when rejecting an authentication. – Matías Fidemraizer May 14 '15 at 12:28
  • Yeah, our authentication was already working too, and we were getting the wrong status as well. We used the authentication code that came 'out of the box' when creating a new project, with authentication. If you did it differently than this, I am not sure why that is not getting called. Where do you validate the login? and set up the claims piece? That is if you do use claims. – Greg P May 14 '15 at 12:32
  • Login is being validated in a derived class of `OAuthAuthorizationServerProvider` – Matías Fidemraizer May 14 '15 at 12:40
  • 1
    I managed to get the middleware working (it was about the order on which the middleware was registered in the app...). BTW, while the 401 code is being set, the response is still HTTP/400. – Matías Fidemraizer May 14 '15 at 13:54
  • 1
    The answer works when app.Use() is written before app.UseOAuthAuthorizationServer. – Rajat Jun 06 '17 at 10:42
  • Keep in mind changing from 400 to 401 is out of alignment with the OAuth2 spec and may trip up clients adhering to OAuth2 – Jerico Sandhorn Aug 10 '18 at 18:14
0

I follow a slightly different approach using System.Security.Authentication.AuthenticationException and an exception middleware.

The AuthenticationServerProvider:

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
    {
        using (UserManager<IdentityUser> userManager = _userManagerFactory())
        {
            IdentityUser user = await userManager.FindAsync(context.UserName, context.Password);

            if (user == null)
            {
                throw new AuthenticationException("The user name or password is incorrect.");
            }
            ....

The middleware:

public class ExceptionMiddleware : OwinMiddleware
    {
        public ExceptionMiddleware(OwinMiddleware next) : base(next)
        {
        }

        public override async Task Invoke(IOwinContext context)
        {
            try
            {
                await Next.Invoke(context);
            }
            catch (Exception ex)
            {
                HandleException(ex, context);
            }
        }

        private void HandleException(Exception ex, IOwinContext owinContext)
        {
            var errorDetails = new ErrorDetails()
            {
                Detail = ex.Message,
                Status = (int)HttpStatusCode.InternalServerError
            };

            switch (ex)
            {
                case AuthenticationException _:
                    errorDetails.Status = (int)HttpStatusCode.Unauthorized;
                    errorDetails.Title = "invalid_grant";
                    break;
                case [..]
                case Exception _:
                    errorDetails.Title = "An unexpected error occured";
                    break;
            }

            var serializedError = errorDetails.ToString();
            Log.Error($"Returning error response: {serializedError}");
            owinContext.Response.StatusCode = errorDetails.Status;
            owinContext.Response.ContentType = "application/json";
            owinContext.Response.Write(serializedError);
        }

        private class ErrorDetails
        {
            public int Status { get; set; }
            public string Title { get; set; }
            public string Detail { get; set; }

            public override string ToString()
            {
                return JsonSerializer.Serialize(this);
            }
        }

The app configuration:

public void Configure(IAppBuilder app)
    ....
        app.Use<ExceptionMiddleware>();
        // Configure auth here
    ....

    }

The result:

enter image description here

MatterOfFact
  • 1,253
  • 2
  • 18
  • 49