Return error on invalid or expired token

蹲街弑〆低调 提交于 2020-04-08 09:13:13

问题


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.


回答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




回答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.




回答3:


If authentication fails (meaning the token is expired) then that layer doesn't set the user, as you said. It's up the the authorization layer (later on) to reject the call. So for your scenario your Web API would need to deny access to an anonymous caller. Use the [Authorize] authorization filter attribute.



来源:https://stackoverflow.com/questions/22841313/return-error-on-invalid-or-expired-token

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!