Restricting Access to Azure Mobile Service Base on Azure Active Directory Group

In my previous post I controlled access to the GetAll method on my base controller by determining whether the authenticated (from AAD) user was a member of the Inspectors AAD group. This is actually quite a slow process and not something you really want to do every session. Ideally I’d like this check to be done once when the user authenticates against the mobile service (which happens after they authenticate against AAD) and for the IsInspector claim to be added to the user identity. Unfortunately for the life of me I can’t work out how to force OWIN into accepting an additional claim – I’m sure there’s a way, but I ended up settling for an alternative approach.

My approach actually improves on two aspects over what I was previously doing. The first is that I implement the checking logic as an attribute which can then be applied to the root controller. The second is that by storing a cookie in the response, I can reduce the need to re-query AAD for the group membership. This solution is based on a couple of great blog posts:

http://www.acupofcode.com/2014/04/general-roles-based-access-control-in-the-net-backend/

http://www.acupofcode.com/2014/03/roles-based-access-control-in-mobile-services-and-azure-active-directory/

The AuthorizeInspector attribute looks as follows:

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class AuthorizeInspector : AuthorizationFilterAttribute
{
    public static ActiveDirectoryClient RetrieveActiveDirectoryClient(string token)
    {
        var baseServiceUri = new Uri(Microsoft.Azure.ActiveDirectory.GraphClient.Constants.ResourceId);
        var activeDirectoryClient =
            new ActiveDirectoryClient(new Uri(baseServiceUri, Constants.ADTenant),
                async () => token);
        return activeDirectoryClient;
    }

    public async override Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
    {
        await base.OnAuthorizationAsync(actionContext, cancellationToken);

        var cookie = HttpContext.Current.Request.Cookies[“IsInspector”];
        var isInspector = cookie != null ? cookie.Value : null;
        if (isInspector != null)
        {
            if (!(bool.Parse(isInspector)))
            {
                actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
            }
            return;
        }

        var controller = actionContext.ControllerContext.Controller as ApiController;
        if (controller == null)
        {
            return;
        }
        var user = controller.User as ServiceUser;

        //var user = User as ServiceUser;
        var aadCreds = (await user.GetIdentitiesAsync()).OfType<AzureActiveDirectoryCredentials>().FirstOrDefault();
        Debug.WriteLine(aadCreds.AccessToken);

        var token = actionContext.Request.Headers.GetValues(Constants.RefreshTokenHeaderKey)
            .FirstOrDefault();

        var auth = new AuthenticationContext(Constants.ADAuthority, false);
        var newToken = await auth.AcquireTokenByRefreshTokenAsync(token,
                Constants.ADNativeClientApplicationClientId, “
https://graph.windows.net”);

        var client = RetrieveActiveDirectoryClient(newToken.AccessToken);
        var grps = await client.Groups.ExecuteAsync();
        var moreGroups = grps.CurrentPage;

        while (moreGroups != null)
        {
            foreach (var grp in grps.CurrentPage)
            {
                if (grp.DisplayName == “Inspectors”)
                {
                    if ((await client.IsMemberOfAsync(grp.ObjectId, aadCreds.ObjectId)) ?? false)
                    {
                        HttpContext.Current.Response.Cookies.Add(new HttpCookie(“IsInspector”, true.ToString()));

                        return;
                    }
                }
            }
            if (grps.MorePagesAvailable)
            {
                grps = await grps.GetNextPageAsync();
                moreGroups = grps.CurrentPage;
            }
            else
            {
                grps = null;
                moreGroups = null;
            }
        }
        HttpContext.Current.Response.Cookies.Add(new HttpCookie(“IsInspector”, false.ToString()));
    }
}

As you can see this follows roughly the same logic for querying AAD group membership. However, this time I’m adding a cookie based on whether the user is an Inspector or not.This attribute can now be applied to the RealEstateBaseTableController.

[AuthorizeInspector]
public class RealEstateBaseTableController<TEntity> : TableController<TEntity>
    where TEntity : class, ITableData
{

One thing to be aware of is that this cookie will persist even if the user logs out. As such, we need some way of associating the cookie with the current user session. It may be that an additional cookie is used to associate the access token with the IsInspector cookie. For example:

public override async Task OnAuthorizationAsync(HttpActionContext actionContext,
    CancellationToken cancellationToken)
{
    await base.OnAuthorizationAsync(actionContext, cancellationToken);

    var controller = actionContext.ControllerContext.Controller as ApiController;
    if (controller == null)
    {
        return;
    }
    var user = controller.User as ServiceUser;

    //var user = User as ServiceUser;
    var aadCreds = (await user.GetIdentitiesAsync()).OfType<AzureActiveDirectoryCredentials>().FirstOrDefault();
    Debug.WriteLine(aadCreds.AccessToken);

    var cookie = HttpContext.Current.Request.Cookies[“IsInspector”];
    var isInspector = cookie != null ? cookie.Value : null;
    var accessTokenCookie = HttpContext.Current.Request.Cookies[“IsInspectorAccessToken”];
    var access_token = accessTokenCookie != null ? accessTokenCookie.Value : null;
    if (isInspector != null && access_token == aadCreds.AccessToken)
    {
        if (!(bool.Parse(isInspector)))
        {
            actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
        }
        return;
    }

    var token = actionContext.Request.Headers.GetValues(Constants.RefreshTokenHeaderKey)
        .FirstOrDefault();

    var auth = new AuthenticationContext(Constants.ADAuthority, false);
    var newToken = await auth.AcquireTokenByRefreshTokenAsync(token,
        Constants.ADNativeClientApplicationClientId, “
https://graph.windows.net”);

    var client = RetrieveActiveDirectoryClient(newToken.AccessToken);
    var grps = await client.Groups.ExecuteAsync();
    var moreGroups = grps.CurrentPage;

    try
    {
        while (moreGroups != null)
        {
            foreach (var grp in grps.CurrentPage)
            {
                if (grp.DisplayName == “Inspectors”)
                {
                    if ((await client.IsMemberOfAsync(grp.ObjectId, aadCreds.ObjectId)) ?? false)
                    {
                        HttpContext.Current.Response.Cookies.Add(new HttpCookie(“IsInspector”, true.ToString()));

                        return;
                    }
                }
            }
            if (grps.MorePagesAvailable)
            {
                grps = await grps.GetNextPageAsync();
                moreGroups = grps.CurrentPage;
            }
            else
            {
                grps = null;
                moreGroups = null;
            }
        }
        HttpContext.Current.Response.Cookies.Add(new HttpCookie(“IsInspector”, false.ToString()));
    }
    finally
    {
        HttpContext.Current.Response.Cookies.Add(new HttpCookie(“IsInspectorAccessToken”, aadCreds.AccessToken));

    }
}

Leave a comment