13

So I have an MVC6 app that includes an identity server (using ThinkTecture's IdentityServer3) and an MVC6 web services application.

In the web services application I am using this code in Startup:

app.UseOAuthBearerAuthentication(options =>
{
    options.Authority = "http://localhost:6418/identity";
    options.AutomaticAuthentication = true;
    options.Audience = "http://localhost:6418/identity/resources";
});

Then I have a controller with an action that has the Authorize attribute.

I have a JavaScript application that authenticates with the identity server, and then uses the provided JWT token to access the web services action.

This works, and I can only access the action with a valid token.

The problem comes when the JWT has expired. What I'm getting is what appears to be a verbose ASP.NET 500 error page, that returns exception information for the following exception:

System.IdentityModel.Tokens.SecurityTokenExpiredException IDX10223: Lifetime validation failed. The token is expired.

I am fairly new to OAuth and securing Web APIs in general, so I may be way off base, but a 500 error doesn't seem appropriate to me for an expired token. It's definitely not friendly for a web service client.

Is this the expected behavior, and if not, is there something I need to do to get a more appropriate response?

Gerald
  • 23,011
  • 10
  • 73
  • 102

1 Answers1

11

Edit: this bug was fixed in ASP.NET Core RC2 and the workaround described in this answer is no longer needed.


Note: this workaround won't work on ASP.NET 5 RC1, due to this other bug. You can either migrate to the RC2 nightly builds or create a custom middleware that catches the exceptions thrown by the JWT bearer middleware and returns a 401 response:

app.Use(next => async context => {
    try {
        await next(context);
    }

    catch {
        // If the headers have already been sent, you can't replace the status code.
        // In this case, throw an exception to close the connection.
        if (context.Response.HasStarted) {
            throw;
        }

        context.Response.StatusCode = 401;
    }
});

Sadly, that's how the JWT/OAuth2 bearer middleware (managed by MSFT) currently works by default, but it should be eventually fixed. You can see this GitHub ticket for more information: https://github.com/aspnet/Security/issues/411

Luckily, you can "easily" work around that by using the AuthenticationFailed notification:

app.UseOAuthBearerAuthentication(options => {
    options.Notifications = new OAuthBearerAuthenticationNotifications {
        AuthenticationFailed = notification => {
            notification.HandleResponse();

            return Task.FromResult<object>(null);
        }
    };
});
Kévin Chalet
  • 39,509
  • 7
  • 121
  • 131
  • 1
    Pinpoint, thanks for the answer. Looking at this method, it says the caller (I'm assuming me) has to handle the full response. It somehow already magically triggers a 401. Is there a way to send a message in the body in the form of json? – damccull Sep 02 '15 at 16:10
  • Though its usually discouraged, yep, it's possible. You can/should use the `ApplyChallenge` notification for that. – Kévin Chalet Sep 02 '15 at 16:16
  • Oooh, why is it discouraged? Also, would I use both challenges or just the one? – damccull Sep 02 '15 at 16:17
  • Because with HTTP, the challenge is already done at the headers level. The OAuth2 bearer specs leverages the standard `WWW-Authenticate` header for that (you can even set a custom error/error_description/error_uri: https://tools.ietf.org/html/rfc6750#section-3). Of course, nothing prevents you from returning a message in the response body, but that would be redundant and - obviously - non-standard. – Kévin Chalet Sep 02 '15 at 16:22
  • I see. Then how would I set a custom error message for the 401? I guess it's not really required, but it'd be helpful to be able to send the reason a user is not authorized. Not an admin, not logged in, not on a valid client... – damccull Sep 02 '15 at 16:29
  • You can use `ApplyChallenge` and create your own header, with the error code you want. That said, the samples you're suggesting - except `not logged in` - are more related to authorization - which should result in a 403 response - than to authentication (= 401 response). – Kévin Chalet Sep 02 '15 at 16:31
  • That makes a lot of sense... I'll swap most of my 401's out for 403's and see about sending a message. This is why I like you. – damccull Sep 02 '15 at 16:34
  • `AuthorizeAttribute`/`AuthorizeFilter` always applies a 401 response, but the authentication middleware automatically "convert" it to 403 response if the user was already authenticated (which is total horror as it breaks the separation of concerns principle, but that's another topic), so you shouldn't have to worry about manually returning 401 or 403. – Kévin Chalet Sep 02 '15 at 16:38
  • I'm probably doing the authentication wrong... It seems to be working though. http://github.com/damccull/MVC6BearerTokens if you're interested in seeing what I'm doing. I'm doing some manual checks when issuing the bearer token to ensure the user is valid, knows password, or has passed a valid refresh token. – damccull Sep 02 '15 at 17:05
  • Finally got around to trying this and it works like a champ. Thanks! – Gerald Oct 07 '15 at 19:59