Return error on invalid or expired token

20,904

Solution 1

I had a similar issue, i think the answer is to late but someone will come here with a similar problem:

I used this nuget package for validate authentication, but i think any method can help: https://www.nuget.org/packages/WebApi.AuthenticationFilter. You can read its documentation in this site https://github.com/mbenford/WebApi-AuthenticationFilter

AuthenticationFilter.cs

public class AuthenticationFilter : AuthenticationFilterAttribute{
public override void OnAuthentication(HttpAuthenticationContext context)
{
    System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
    var ci = context.Principal.Identity as ClaimsIdentity;

    //First of all we are going to check that the request has the required Authorization header. If not set the Error
    var authHeader = context.Request.Headers.Authorization;
    //Change "Bearer" for the needed schema
    if (authHeader == null || authHeader.Scheme != "Bearer")
    {
        context.ErrorResult = context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
            new { Error = new { Code = 401, Message = "Request require authorization" } });
    }
    //If the token has expired the property "IsAuthenticated" would be False, then set the error
    else if (!ci.IsAuthenticated)
    {
        context.ErrorResult = new AuthenticationFailureResult("unauthorized", context.Request,
            new { Error = new { Code = 401, Message = "The Token has expired" } });
    }
}}

AuthenticationFailureResult.cs

public class AuthenticationFailureResult : IHttpActionResult{
private object ResponseMessage;
public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request, object responseMessage)
{
    ReasonPhrase = reasonPhrase;
    Request = request;
    ResponseMessage = responseMessage;
}

public string ReasonPhrase { get; private set; }

public HttpRequestMessage Request { get; private set; }

public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
{
    return Task.FromResult(Execute());
}

private HttpResponseMessage Execute()
{
    HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
    System.Net.Http.Formatting.MediaTypeFormatter jsonFormatter = new System.Net.Http.Formatting.JsonMediaTypeFormatter();
    response.Content = new System.Net.Http.ObjectContent<object>(ResponseMessage, jsonFormatter);
    response.RequestMessage = Request;
    response.ReasonPhrase = ReasonPhrase;
    return response;
}}

Response examples:

{"Error":{"Code":401,"Message":"Request require authorization"}}

{"Error":{"Code":401,"Message":"The Token has expired"}}

Fonts and inspiration documentation:

//github.com/mbenford/WebApi-AuthenticationFilter

//www.asp.net/web-api/overview/security/authentication-filters

Solution 2

Yeah, I did not find 'good' solution for this,

I also don't see a way to plug in my own implementation of the OAuthBearerAuthenticationHandler in the OAuthBearerAuthenticationMiddleware.

Apparently my best and cleanest shot would be to reimplement the whole middleware, but this also seems very overkill.

Agreed, but that's what I did (before reading your post). I copy & pasted three owin classes, and made it so that it sets property in Owins context, which can be later checked by other handlers.

public static class OAuthBearerAuthenticationExtensions
{
    public static IAppBuilder UseOAuthBearerAuthenticationExtended(this IAppBuilder app, OAuthBearerAuthenticationOptions options)
    {
        if (app == null)
            throw new ArgumentNullException(nameof(app));

        app.Use(typeof(OAuthBearerAuthenticationMiddlewareExtended), app, options);
        app.UseStageMarker(PipelineStage.Authenticate);
        return app;
    }
}

internal class OAuthBearerAuthenticationHandlerExtended : AuthenticationHandler<OAuthBearerAuthenticationOptions>
{
    private readonly ILogger _logger;
    private readonly string _challenge;

    public OAuthBearerAuthenticationHandlerExtended(ILogger logger, string challenge)
    {
        _logger = logger;
        _challenge = challenge;
    }

    protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
    {
        try
        {
            // Find token in default location
            string requestToken = null;
            string authorization = Request.Headers.Get("Authorization");
            if (!string.IsNullOrEmpty(authorization))
            {
                if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                {
                    requestToken = authorization.Substring("Bearer ".Length).Trim();
                }
            }

            // Give application opportunity to find from a different location, adjust, or reject token
            var requestTokenContext = new OAuthRequestTokenContext(Context, requestToken);
            await Options.Provider.RequestToken(requestTokenContext);

            // If no token found, no further work possible
            if (string.IsNullOrEmpty(requestTokenContext.Token))
            {
                return null;
            }

            // Call provider to process the token into data
            var tokenReceiveContext = new AuthenticationTokenReceiveContext(
                Context,
                Options.AccessTokenFormat,
                requestTokenContext.Token);

            await Options.AccessTokenProvider.ReceiveAsync(tokenReceiveContext);
            if (tokenReceiveContext.Ticket == null)
            {
                tokenReceiveContext.DeserializeTicket(tokenReceiveContext.Token);
            }

            AuthenticationTicket ticket = tokenReceiveContext.Ticket;
            if (ticket == null)
            {
                _logger.WriteWarning("invalid bearer token received");
                Context.Set("oauth.token_invalid", true);
                return null;
            }

            // Validate expiration time if present
            DateTimeOffset currentUtc = Options.SystemClock.UtcNow;

            if (ticket.Properties.ExpiresUtc.HasValue &&
                ticket.Properties.ExpiresUtc.Value < currentUtc)
            {
                _logger.WriteWarning("expired bearer token received");
                Context.Set("oauth.token_expired", true);
                return null;
            }

            // Give application final opportunity to override results
            var context = new OAuthValidateIdentityContext(Context, Options, ticket);
            if (ticket != null &&
                ticket.Identity != null &&
                ticket.Identity.IsAuthenticated)
            {
                // bearer token with identity starts validated
                context.Validated();
            }
            if (Options.Provider != null)
            {
                await Options.Provider.ValidateIdentity(context);
            }
            if (!context.IsValidated)
            {
                return null;
            }

            // resulting identity values go back to caller
            return context.Ticket;
        }
        catch (Exception ex)
        {
            _logger.WriteError("Authentication failed", ex);
            return null;
        }
    }

    protected override Task ApplyResponseChallengeAsync()
    {
        if (Response.StatusCode != 401)
        {
            return Task.FromResult<object>(null);
        }

        AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);

        if (challenge != null)
        {
            OAuthChallengeContext challengeContext = new OAuthChallengeContext(Context, _challenge);
            Options.Provider.ApplyChallenge(challengeContext);
        }

        return Task.FromResult<object>(null);
    }
}


public class OAuthBearerAuthenticationMiddlewareExtended : AuthenticationMiddleware<OAuthBearerAuthenticationOptions>
{
    private readonly ILogger _logger;
    private readonly string _challenge;

    /// <summary>
    /// Bearer authentication component which is added to an OWIN pipeline. This constructor is not
    ///             called by application code directly, instead it is added by calling the the IAppBuilder UseOAuthBearerAuthentication
    ///             extension method.
    /// 
    /// </summary>
    public OAuthBearerAuthenticationMiddlewareExtended(OwinMiddleware next, IAppBuilder app, OAuthBearerAuthenticationOptions options)
      : base(next, options)
    {
        _logger = AppBuilderLoggerExtensions.CreateLogger<OAuthBearerAuthenticationMiddlewareExtended>(app);
        _challenge = string.IsNullOrWhiteSpace(Options.Challenge) ? (!string.IsNullOrWhiteSpace(Options.Realm) ? "Bearer realm=\"" + this.Options.Realm + "\"" : "Bearer") : this.Options.Challenge;

        if (Options.Provider == null)
            Options.Provider = new OAuthBearerAuthenticationProvider();

        if (Options.AccessTokenFormat == null)
            Options.AccessTokenFormat = new TicketDataFormat(
                Microsoft.Owin.Security.DataProtection.AppBuilderExtensions.CreateDataProtector(app, typeof(OAuthBearerAuthenticationMiddleware).Namespace, "Access_Token", "v1"));

        if (Options.AccessTokenProvider != null)
            return;

        Options.AccessTokenProvider = new AuthenticationTokenProvider();
    }

    /// <summary>
    /// Called by the AuthenticationMiddleware base class to create a per-request handler.
    /// 
    /// </summary>
    /// 
    /// <returns>
    /// A new instance of the request handler
    /// </returns>
    protected override AuthenticationHandler<OAuthBearerAuthenticationOptions> CreateHandler()
    {
        return new OAuthBearerAuthenticationHandlerExtended(_logger, _challenge);
    }
}

Then I wrote my own authorization filter, which will be applied globally:

public class AuthorizeAttributeExtended : AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(HttpActionContext actionContext)
    {
        var tokenHasExpired = false;
        var owinContext = OwinHttpRequestMessageExtensions.GetOwinContext(actionContext.Request);
        if (owinContext != null)
        {
            tokenHasExpired = owinContext.Environment.ContainsKey("oauth.token_expired");
        }

        if (tokenHasExpired)
        {
            actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
                new
                {
                    error = "invalid_token",
                    error_message = "The Token has expired"
                });
        }
        else
        {
            actionContext.Response = new AuthenticationFailureMessage("unauthorized", actionContext.Request,
                new
                {
                    error = "invalid_request",
                    error_message = "The Token is invalid"
                });
        }
    }
}

public class AuthenticationFailureMessage : HttpResponseMessage
{
    public AuthenticationFailureMessage(string reasonPhrase, HttpRequestMessage request, object responseMessage)
        : base(HttpStatusCode.Unauthorized)
    {
        MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();

        Content = new ObjectContent<object>(responseMessage, jsonFormatter);
        RequestMessage = request;
        ReasonPhrase = reasonPhrase;
    }
}

my WebApiConfig:

config.Filters.Add(new AuthorizeAttributeExtended());

How my configureOAuth looks like:

public void ConfigureOAuth(IAppBuilder app)
{
    app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);

    OAuthBearerOptions = new OAuthBearerAuthenticationOptions()
    {

    };

    OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
    {
        AllowInsecureHttp = true,
        TokenEndpointPath = new PathString("/token"),
        AccessTokenExpireTimeSpan = TimeSpan.FromSeconds(10),

        Provider = new SimpleAuthorizationServerProvider(),
        RefreshTokenProvider = new SimpleRefreshTokenProvider(),
        AuthenticationMode =  AuthenticationMode.Active
    };

    FacebookAuthOptions = new CustomFacebookAuthenticationOptions();

    app.UseFacebookAuthentication(FacebookAuthOptions);
    app.UseOAuthAuthorizationServer(OAuthServerOptions);

    app.UseOAuthBearerAuthenticationExtended(OAuthBearerOptions);
}

I will try & get this to main branch of oAuth middleware, it seems like an obvious use case, unless I am missing something.

Solution 3

I came across this problem recently. We wanted to return a JSON message if the user's access token had expired, allowing the consumer web application to silently refresh the access token and re-issue the API request. We also didn't want to rely on the exceptions thrown for token lifetime validation.

Not wanting to re-implement any middleware, we specified the Provider option inside JwtBearerAuthenticationOptions and added a delegate to handle the OnRequestTokenMethod. The delegate checks to see if it can read the token passed to the middleware and sets a boolean inside the OWIN context if it's expired.

app.UseJwtBearerAuthentication(
             new JwtBearerAuthenticationOptions
             {
                 AuthenticationMode = AuthenticationMode.Active,
                 TokenValidationParameters = tokenValidationParameters,                                             
                 Provider = new OAuthBearerAuthenticationProvider
                 {                         
                     OnRequestToken = (ctx) =>
                     {                             
                         if (!string.IsNullOrEmpty(ctx.Token))
                         {
                             JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
                             if (handler.CanReadToken(ctx.Token))
                             {
                                 JwtSecurityToken jwtToken = handler.ReadJwtToken(ctx.Token);
                                 
                                 if (jwtToken.IsExpired())
                                     ctx.OwinContext.Set<bool>("expiredToken", true);
                             }                                                                  
                         }

                         return Task.CompletedTask;
                     }
                 }
             });            

For convenience I added a quick extension method to check if a JWT expired:

    public static class JwtSecurityTokenExtensions
    {        
        public static bool IsExpired (this JwtSecurityToken token)
        {
            if (DateTime.UtcNow > token.ValidTo.ToUniversalTime())
                return true;

            return false;
        }
    }

We ended up using a middleware to check on the state of that boolean:

app.Use((context, next) =>
        {
            bool expiredToken = context.Get<bool>("expiredToken");
            
            if (expiredToken)
            {
                // do stuff
            }

            return next.Invoke();
        });
        app.UseStageMarker(PipelineStage.Authenticate);

Not exactly the most efficient code, since we're parsing the token again after the middleware already did and also introducing a new middleware to act on the result of the check, but it's a fresh perspective nonetheless.

Share:
20,904
user3137652
Author by

user3137652

Updated on July 09, 2022

Comments

  • user3137652
    user3137652 almost 2 years

    I'm trying to implement OAuth Bearer Authentication with Owin. When an invalid or expired token is passed, the default implementation is to log this as a warning and just don't set an Identity. I however would like to reject the whole request with an error in this case. But how would I do this?

    After digging through the code I found out that in OAuthBearerAuthenticationHandler it will parse the token using a fallback mechanism when the provided AuthenticationTokenProvider did not parse any ticket (like the default implementation). This handler will log a warning when the token could not be parsed to any ticket or when it expired.

    But I can't find any place to plug in my own logic to what happens when the token is invalid or expired. I could theoretically check this on my own in the AuthenticationTokenProvider, but then I would have to reimplement the logic (= copy it over) for creating and reading the token. Also this seems just out of place, as this class seems to be only responsible for creating and parsing tokens. I also don't see a way to plug in my own implementation of the OAuthBearerAuthenticationHandler in the OAuthBearerAuthenticationMiddleware.

    Apparently my best and cleanest shot would be to reimplement the whole middleware, but this also seems very overkill.

    What do I overlook? How would I go on about this the best?

    edit:

    For clarification. I know by not setting an identity the request will be rejected with 401 Unauthorized later in the Web API. But I personally see this as really bad style, silently swallowing an erroneous access token without any notification. This way you don't get to know that your token is crap, you just get to know you're not authorized.

  • user3137652
    user3137652 about 10 years
    Thank you for your response, but this is no answer to my question. I'm specifically asking on how to reject invalid access token. I understand that the Web API itself rejects with a 401 Unauthorized, but I consider this very bad style. If an INVALID or EXPIRED access token is passed, the response should be a notification that the access token is INVALID or EXPIRED - not that the user is simply not authorized.
  • user3137652
    user3137652 about 10 years
    I would be glad if you could remove your answer, since it does not answer my question. I edited my question and made clear that I am aware of this, and that this is not what I want.
  • Brock Allen
    Brock Allen about 10 years
    Sorry you don't like the answer, but it's the proper design. Authentication is separate from authorization. Dan Roth from Microsoft even points this out in this recent BUILD session: channel9.msdn.com/Events/Build/2014/3-603
  • user3137652
    user3137652 about 10 years
    I never argued that, I know that authentication and authorization are two different things. But I simply want to reject invalid authentication attempts with an error, not silently swallow them. It is also pointless to log a warning (like the implementation does), if I can't react to it properly.
  • Jerico Sandhorn
    Jerico Sandhorn over 5 years
    Its good to have the extra code to discern the reason for the 401. We needed it to distinguish between expired, invalid and insufficient permissions
  • Jerico Sandhorn
    Jerico Sandhorn over 5 years
    The only issue is that there are many other reasons IsAuthorized can be false instead of expired