OAuth token expiration in MVC6 app
Asked Answered
B

1

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?

Broucek answered 31/8, 2015 at 23:45 Comment(2)
I'm having this same issue. Would love to know where to catch the exception and return a 401.Scorcher
Are you still seeing this issue with the suggested fix?Bradbury
H
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);
        }
    };
});
Horizontal answered 2/9, 2015 at 12:45 Comment(10)
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?Scorcher
Though its usually discouraged, yep, it's possible. You can/should use the ApplyChallenge notification for that.Bradbury
Oooh, why is it discouraged? Also, would I use both challenges or just the one?Scorcher
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: 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.Bradbury
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...Scorcher
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).Bradbury
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.Scorcher
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.Bradbury
I'm probably doing the authentication wrong... It seems to be working though. 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.Scorcher
Finally got around to trying this and it works like a champ. Thanks!Broucek

© 2022 - 2024 — McMap. All rights reserved.