Search code examples
asp.netasp.net-corejwtopenid-connectaspnet-contrib

Custom Lifetime Validation With AspNet.Security.OpenIdConnect.Server (ASP.NET vNext)


I am using Visual Studio 2015 Enterprise Update 1 and ASP.NET vNext rc1-update1 to issue and consume JWT tokens as described here.

In our implementation we want to control token lifetime validation.

We tried several approaches, all of which had undesirable side effects. For example in one attempt we took over the TokenValidationParameters.TokenValidationParameters.LifetimeValidator event in the Configure method:

app.UseJwtBearerAuthentication
(
    options => 
    {
        options.TokenValidationParameters = new TokenValidationParameters()
        {
            LifetimeValidator = (DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) =>
            {
                // Pretend to do custom validation
                return false;
            }
        };
    }
);

That event causes validation to fail as we'd like but the client receives a 500 error whereas we would like to return a 400-series error and a small payload instead.

In another attempt we tried various implementations of TokenValidationParameters.Events, such as inspecting claims in the ValidatedToken event but found we were unable to prevent the middleware from invoking the controller action short of throwing an exception which got us back to the 500 error problem.

So my questions are:

  • What what is the best practices for taking over lifetime validation with OIDC?

  • Can we force OIDC not to include certain lifetime claims in the token like "nbf" since we won't need them anyway?


Solution

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


    It's a known bug. Sadly, the workaround you could use in beta8 no longer works in RC1.

    Your only option is to write a middleware catching the exception to prevent the server from returning a 500 response. Of course, it's ugly and will potentially hide important exceptions, but it's the only known workaround that works with RC1.

    Here's an example (make sure to register it before the JWT bearer middleware):

    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;
        }
    });