SteinTech
SteinTech

Reputation: 4068

Get reference to requested Controller and Action in custom middleware in ASP.NET Core

I'm developing a custom middleware for authenticating clients that invokes an API.

I use an attribute to define if an Action requires authentication, but I can't figure out how to get a reference to the requested Controller and Action inside the Invoke method.

Below is my code so far

AuthenticateClient.cs:

public class AuthenticateClient
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly GenericUnitOfWork _worker;

    public AuthenticateClient(RequestDelegate next, ApiDbContext db, IHttpContextAccessor httpContext, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<Utility.LCLog.Settings> settings)
    {
        _next = next;
        _logger = loggerFactory.CreateLogger(settings.Value.ApplicationName);
        _worker = new GenericUnitOfWork(new AppHelper(httpContext, db, env));
    }

    public async Task Invoke(HttpContext context)
    {
        if (!context.Request.Headers.Keys.Contains("ClientAuth"))
        {
            _logger.LogWarning("ClientAuth missing in request", new string[] { "Host: " + context.Request.Host, "IP: " + context.Request.HttpContext.Connection.RemoteIpAddress });

            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("ClientAuth missing from request header values");

            return;
        }
        else
        {
            string[] tmp = context.Request.Headers["ClientAuth"].ToString().Split("/");

            if (tmp.Length != 2)
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync("The format of the ClientAuth value is wrong");

                return;
            }
            else
            {
                Client client;
                string key, pass;

                key = tmp[0];
                pass = tmp[1];

                client = await _worker.GetRepo<Client>().SingleOrDefault(clnt => clnt.Active && clnt.Key.Equals(key) && clnt.Password.Equals(pass));

                if (client == null)
                {
                    _logger.LogWarning("Client authentication failed", new string[] { "Key: " + key, "Password: " + pass, "Host: " + context.Request.Host, "IP: " + context.Request.HttpContext.Connection.RemoteIpAddress });

                    context.Response.StatusCode = 401;
                    await context.Response.WriteAsync("Authentication failed");

                    return;
                }
            }                
        }

        await _next.Invoke(context);
    }
}

ClientAuthenticationAttribute.cs:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ClientAuthenticationAttribute : Attribute
{
    private readonly bool _authRequired;

    public ClientAuthenticationAttribute(bool authRequired = true)
    {
        _authRequired = authRequired;
    }

    public bool AuthRequired { get { return _authRequired; } }
}

Upvotes: 4

Views: 2898

Answers (1)

Tomas Aschan
Tomas Aschan

Reputation: 60664

I'd recommend you to split your logic for authentication and authorization and keep them in different places.

To recap from here:

  • Authentication is the process of verifying who you are.

  • Authorization is the process of verifying that, given that we know who you are, you have access to the specific resource.

What you're currently trying to do, is to both authenticate and authorize your user in the middleware component. Although you could probably get it to work by moving all such logic into filters which you register with the api framework (be it ASP.NET Core MVC, Web API 2 or something else), that would mean that none of your other middleware components have access to the user data (which, I'm guessing, is one of the reasons you chose to implement it in a middleware in the first place).

Given your new knowledge of the separation of authentication and authorization, a possible solution would be to do the following:

Middleware for authentication only

In your middleware, concern yourself only with authentication, and leave authorization up to components later in the pipeline. In practice, this means that your middleware should do the following:

  1. Look for user tokens, cookies or whatever you use for the users to authenticate their request
  2. If not present, treat the request as anonymous, and call the next pipeline component without attaching a user to the request context.
  3. If a valid token is present, resolve the user data from it (e.g. parse the user's claims from a JWT, look up roles in a database, etc...) and store it on the request context. I've found it useful both to create an IPrincipal and set context.Request.User to it, as well as adding information to the context dictionary directly.
  4. With the user registered in the request context, call the next pipeline component.

Authorization assuming an authenticated user

You can now re-write your authorization logic to assume that there's already an authenticated user registered on the request context.

In an ASP.NET Web API 2 application, you'd implement a custom filter attribute inheriting from AuthorizationFilterAttribute, to make sure it runs first of the filters. In my current application, for example, we have the following attribute to authorize that a user has a specific claim. Note that it doesn't do any work to figure out who the user is; if a user is not attached to the context, the response is simply Unauthorized. You could be more sophisticated here, and treat anonymous requests differently from authenticated requests for users who lack access, and, for example, redirect anonymous requests to the login form, while redirecting users lacking access to an error page stating as much.

[AttributeUsage(validOn: AttributeTargets.Method)]
public class AuthorizeClaimsFilterAttribute : AuthorizationFilterAttribute
{
    public AuthorizeClaimsFilterAttribute(string claimType, string claimValue)
    {
        ClaimType = claimType;
        ClaimValue = claimValue;
    }

    public string ClaimType { get; }
    public string ClaimValue { get; }

    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (!(actionContext.RequestContext.Principal is ClaimsPrincipal principal)
            || !principal.HasClaim(x => x.Type == ClaimType && x.Value == ClaimValue))
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }
}

To use it, we just decorate the action method with it:

[AuthorizeClaimsFilter("urn:ourapp:claims:admin", true)]
public IHttpActionResults OnlyAdminsCanAccess() { /* ... */ }

Upvotes: 5

Related Questions