Skip to main content

Adding ‘Deny’ functionality to AuthorizeAttribute in Asp.Net Web API

For the web service project I’m working on at the moment I need to be able to treat authorization differently based on the hostname of the URL that requests are made through.

To state more clearly – these web services will have a ‘sandbox’ mode in addition to the real mode, and the mode a request will operate under is determined as part of the controller-selection phase early in the Web API request lifecycle.  So, say that my web services will be hosted on services.acme.com; the sandbox will simply be sandbox.services.acme.com.

Please note – a discussion of how this is implemented is entirely outside the scope of this article; but I’ll just say that I’ve developed an in-house multi-tenancy layer for both MVC 4 and Web API that allows us to define ‘brands’ and, under those, you can then redefine content, controllers, and even the DI container that is used.

These services are going to require caller-level authentication for most operations via SCRAM Authentication (RFC 5802), and as such most controllers or actions will be decorated with the AuthorizeAttribute:

[Authorize(Roles = APICallerIdentity.Authenticated_Role)]

The value being passed to the Roles member there is just a constant I’m using to keep things consistent.

Now here’s the thing – in Sandbox mode you will be able to access many of the operations as a Guest caller (i.e. without needing to undertake authentication) but in the live mode you will not.  You can still run through the SCRAM Authentication process, of course, and once you do you will have an access token that can be used to authenticate future requests so that you are no longer treated as a guest.

To set this up, a filter that runs earlier in the pipeline (registered via the global HttpConfiguration.Filters collection) – which is responsible for authenticating the request – asks the current mode if guest access is enabled.  If it is, and no credentials are found in the request, then the guest user is set to the current thread’s identity with both the Guest_Role and Authenticated_Role roles set.  Thus, all the methods that require authentication normally can still work in sandbox mode.

I then started work on an operation yesterday that I only ever want to be accessible to non-guest authenticated callers – regardless of whether we’re in sandbox or live mode – where was AuthorizeAttribute now!?

The simple fact is – it can’t do that, but it’s ridiculously simple to subclass it and make it so it can – here’s the full listing of AuthorizeExAttribute:

/// <summary>
/// Extends core AuthorizeAttribute to support denying users and roles.
///
/// An error occurs during authorization if a user or role is found in
/// both allow and deny rules.
/// </summary>
public class AuthorizeExAttribute : AuthorizeAttribute
{
    private static readonly string[] _emptyArray = new string[0];

    private static string[] SplitString(string value)
    {
        if (value == null)
            return _emptyArray;

        return (from s in value.Split(",".ToCharArray(),
                            StringSplitOptions.RemoveEmptyEntries)
                        select s.Trim()).ToArray();
    }

    //initialised in constructor
    private readonly Lazy<bool> _isValid;

    private string _denyUsers;
    private string[] _denyUsersSplit = _emptyArray;

    /// <summary>
    /// Gets or sets the users that are to be denied access.
    /// Note: if any of these are also present in the base Users property,
    /// then an error occurs during authorization.
    /// </summary>
    public string DenyUsers
    {
        get
        {
            return _denyUsers ?? string.Empty;
        }
        set
        {
            _denyUsers = value;
            _denyUsersSplit = SplitString(value);
        }
    }

    private string _denyRoles;
    private string[] _denyRolesSplit = _emptyArray;
    /// <summary>
    /// Gets or sets the roles that are to be denied access.
    /// Note: if any of these are also present in the base Users property,
    /// then an error occurs during authorization.
    /// However, if a user is allowed access, but has a role that is denied
    /// access, the deny rule wins.
    /// </summary>
    public string DenyRoles
    {
        get
        {
            return _denyRoles ?? string.Empty;
        }
        set
        {
            _denyRoles = value;
            _denyRolesSplit = SplitString(value);
        }
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="AuthorizeExAttribute"/>
    /// class.
    /// </summary>
    public AuthorizeExAttribute()
    {
        _isValid = new Lazy<bool>(() =>
        {
            //have to re-split the base Users and Roles (if this were
            //implemented within the AuthorizeAttribute this would not
            //be necessary)
            var usersSplit = SplitString(Users);
            var rolesSplit = SplitString(Roles);

            return !_denyUsersSplit.Any(u => usersSplit.Contains(u)) &&
                        !_denyRolesSplit.Any(r => rolesSplit.Contains(r));
        });
    }

    protected override bool IsAuthorized(
            System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (!_isValid.Value)
            throw new InvalidOperationException(
                "One or more users or roles appear in both deny and allow rules");

        var baseResult = base.IsAuthorized(actionContext);
        if (!baseResult)
            return false;
        //since it returned true we know there is an authenticated user.
        var user = Thread.CurrentPrincipal;
        if (_denyUsersSplit.Length > 0 && _denyUsersSplit.Contains(
                    user.Identity.Name, StringComparer.OrdinalIgnoreCase))
        {
            return false;
        }

        if (_denyRolesSplit.Length > 0 && _denyRolesSplit.Any(user.IsInRole))
        {
            return false;
        }

        return true;
    }
}

Please note that the coding style here is intended to mirror that of the core AuthorizeAttribute class – you can see what I mean by looking at the current version on codeplex – because, as the comments mention, this code could easily be merged into that (I know I can submit this myself – but I already have one pull request on the go – and still haven’t gotten around to putting the tests in that are needed to get that one included!).

Notice that an error occurs if a user or role is present in both the allow and deny lists.  Notice also that a ‘deny role’ rule takes precedence over an allow user or role rule.  So if ‘Jimmy’ is allowed, but has the denied role ‘BannedUsers’, then Jimmy will have to work on getting himself un-banned…

Similarly if Jimmy has the ‘AllowedUsers’ role, but still has the ‘BannedUsers’ role, then it’s back to trying make friends with us for Jimmy.

With this in place – all I have to do now to allow authenticated users but deny authenticated guest users is to change my use of AuthorizeAttribute to:

[AuthorizeEx(Roles = APICallerIdentity.Authenticated_Role,
    DenyRoles=APICallerIdentity.Guest_Role)]

And now authorization is denied for the action or controller on which it is applied if the caller is authenticated, but as a guest.

Happy coding!

Comments

Popular posts from this blog

Asp.Net 2 and 4 default application pool generates CS0016 IIS7.5

Before I start – if you’ve found a bunch of other articles about this around the net, tried the fixes that are mentioned and still not getting any joy – then read on – you might find this solves your problem. Earlier today I discovered that when I run any ASP.Net 2 or 4 application through IIS7.5 using the default application pools (which use ApplicationPoolIdentity) on Windows 2008 R2 x64 I get an error message similar to this:   Server Error in '/MvcApplication31' Application. Compilation Error Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately. Compiler Error Message: CS0016: Could not write to output file 'c:\Windows\Microsoft.NET\Framework64\v4.0.30319\Temporary ASP.NET Files\mvcapplication31\222b4fe6\4e80a86\App_global.asax.clb4bsnc.dll' -- 'The directory name is invalid. ' Source Error: [No relevant source ...

Serializing to attributes in WCF with DataContractSerializer

It’s a common problem – you want to return an object from a WCF service as XML, but you either want, or need, to deliver some or all of the property values as XML Attributes instead of XML Elements; but you can’t because the DataContractSerializer doesn’t support attributes (you’re most likely to have seen this StackOverflow QA if you’ve done a web search).  Most likely you’ve then migrated all your WCF service code to using the XmlSerializer (with all the XmlElement/XmlAttribute/XmlType attributes et al) – and you’ve cursed loudly. Well, I’m here to rescue you, because it is possible – and the answer to the problem is actually inferred from the MSDN article entitled ‘ Types supported by the Data Contract Serializer ’. The example I’m going to give is purely for illustration purposes only.  I don’t have a lot of time, so work with me! Create a new Asp.Net WCF service application, you can use Cassini as your web server (probably easier – otherwise you might have to enable...

Shameless plug - Use the new JobServe Web API to search for jobs your way

As my signature states - I work for JobServe in the UK.  Over the past few months I have been working on a new REST API (using the ASP.Net Web API from pre-beta to RTM) and it is now in official public beta. Now of course, this isn't just so your good selves can start running your own job searches using whatever web-enabled client takes your fancy, but that is one of the cool benefits that has come out of it - and that's why we're calling it a public beta. At the time of writing, you can use the API to run almost any job search that you can on the website.  As you would expect, you can get the results in XML or JSON (also GET requests with search parameters in the query-string are supported as well).  Note that JSONP is not currently supported - but it's slated for a future release. Sounds great, how do I get in? In order to get cracking with this - you need to request an API token .  This is not an automated process but we should be able to get you set up i...