Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using Forms Authentication with Web API

I've got a Web Forms application which I'm trying to use the new Web API beta with. The endpoints I'm exposing should only be available to an authenticated user of the site since they're for AJAX use. In my web.config I have it set to deny all users unless they're authenticated. This works as it should with Web Forms but does not work as expected with MVC or the Web API.

I've created both an MVC Controller and Web API Controller to test with. What I'm seeing is that I can't access the MVC or Web API endpoints untill I authenticate but then I can continue hitting those endpoints, even after closing my browser and recyling the app pool. But if I hit one of my aspx pages, which sends me back to my login page, then I can't hit the MVC or Web API endpoints untill I authenticate again.

Is there a reason why MVC and Web API are not functioning as my ASPX pages are once my session is invalidated? By the looks of it only the ASPX request is clearing my Forms Authentication cookie, which I'm assuming is the issue here.

like image 859
Brian Surowiec Avatar asked Apr 26 '12 00:04

Brian Surowiec


1 Answers

If your web API is just used within an existing MVC application, my advice is to create a custom AuthorizeAttribute filter for both your MVC and WebApi controllers; I create what I call an "AuthorizeSafe" filter, which blacklists everything by default so that if you forget to apply an authorization attribute to the controller or method, you are denied access (I think the default whitelist approach is insecure).

Two attribute classes are provided for you to extend; System.Web.Mvc.AuthorizeAttribute and System.Web.Http.AuthorizeAttribute; the former is used with MVC forms authentication and the latter also hooks into forms authentication (this is very nice because it means you don't have to go building a whole separate authentication architecture for your API authentication and authorization). Here's what I came up with - it denies access to all MVC controllers/actions and WebApi controllers/actions by default unless an AllowAnonymous or AuthorizeSafe attribute is applied. First, an extension method to help with custom attributes:

public static class CustomAttributeProviderExtensions {
    public static List<T> GetCustomAttributes<T>(this ICustomAttributeProvider provider, bool inherit) where T : Attribute {
        List<T> attrs = new List<T>();

        foreach (object attr in provider.GetCustomAttributes(typeof(T), false)) {
            if (attr is T) {
                attrs.Add(attr as T);
            }
        }

        return attrs;
    }
}

The authorization helper class that both the AuthorizeAttribute extensions use:

public static class AuthorizeSafeHelper {
    public static AuthActionToTake DoSafeAuthorization(bool anyAllowAnonymousOnAction, bool anyAllowAnonymousOnController, List<AuthorizeSafeAttribute> authorizeSafeOnAction, List<AuthorizeSafeAttribute> authorizeSafeOnController, out string rolesString) {
        rolesString = null;

        // If AllowAnonymousAttribute applied to action or controller, skip authorization
        if (anyAllowAnonymousOnAction || anyAllowAnonymousOnController) {
            return AuthActionToTake.SkipAuthorization;
        }

        bool foundRoles = false;
        if (authorizeSafeOnAction.Count > 0) {
            AuthorizeSafeAttribute foundAttr = (AuthorizeSafeAttribute)(authorizeSafeOnAction.First());
            foundRoles = true;
            rolesString = foundAttr.Roles;
        }
        else if (authorizeSafeOnController.Count > 0) {
            AuthorizeSafeAttribute foundAttr = (AuthorizeSafeAttribute)(authorizeSafeOnController.First());
            foundRoles = true;
            rolesString = foundAttr.Roles;
        }

        if (foundRoles && !string.IsNullOrWhiteSpace(rolesString)) {
            // Found valid roles string; use it as our own Roles property and auth normally
            return AuthActionToTake.NormalAuthorization;
        }
        else {
            // Didn't find valid roles string; DENY all access by default
            return AuthActionToTake.Unauthorized;
        }
    }
}

public enum AuthActionToTake {
    SkipAuthorization,
    NormalAuthorization,
    Unauthorized,
}

The two extension classes themselves:

public sealed class AuthorizeSafeFilter : System.Web.Mvc.AuthorizeAttribute {
    public override void OnAuthorization(AuthorizationContext filterContext) {
        if (!string.IsNullOrEmpty(this.Roles) || !string.IsNullOrEmpty(this.Users)) {
            throw new Exception("This class is intended to be applied to an MVC web API application as a global filter in RegisterWebApiFilters, not applied to individual actions/controllers.  Use the AuthorizeSafeAttribute with individual actions/controllers.");
        }

        string rolesString;
        AuthActionToTake action = AuthorizeSafeHelper.DoSafeAuthorization(
            filterContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(false).Count() > 0,
            filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>(false).Count() > 0,
            filterContext.ActionDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>(false),
            filterContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>(false),
            out rolesString
        );

        string rolesBackup = this.Roles;
        try {
            switch (action) {
                case AuthActionToTake.SkipAuthorization:
                    return;

                case AuthActionToTake.NormalAuthorization:
                    this.Roles = rolesString;
                    base.OnAuthorization(filterContext);
                    return;

                case AuthActionToTake.Unauthorized:
                    filterContext.Result = new HttpUnauthorizedResult();
                    return;
            }
        }
        finally {
            this.Roles = rolesBackup;
        }
    }
}

public sealed class AuthorizeSafeApiFilter : System.Web.Http.AuthorizeAttribute {
    public override void OnAuthorization(HttpActionContext actionContext) {
        if (!string.IsNullOrEmpty(this.Roles) || !string.IsNullOrEmpty(this.Users)) {
            throw new Exception("This class is intended to be applied to an MVC web API application as a global filter in RegisterWebApiFilters, not applied to individual actions/controllers.  Use the AuthorizeSafeAttribute with individual actions/controllers.");
        }

        string rolesString;
        AuthActionToTake action = AuthorizeSafeHelper.DoSafeAuthorization(
            actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0,
            actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Count > 0,
            actionContext.ActionDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>().ToList(),
            actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<AuthorizeSafeAttribute>().ToList(),
            out rolesString
        );

        string rolesBackup = this.Roles;
        try {
            switch (action) {
                case AuthActionToTake.SkipAuthorization:
                    return;

                case AuthActionToTake.NormalAuthorization:
                    this.Roles = rolesString;
                    base.OnAuthorization(actionContext);
                    return;

                case AuthActionToTake.Unauthorized:
                    HttpRequestMessage request = actionContext.Request;
                    actionContext.Response = request.CreateResponse(HttpStatusCode.Unauthorized);
                    return;
            }
        }
        finally {
            this.Roles = rolesBackup;
        }
    }
}

And finally, the attribute that can be applied to methods/controllers to allow users in certain roles to access them:

public class AuthorizeSafeAttribute : Attribute {
    public string Roles { get; set; }
}

Then we register our "AuthorizeSafe" filters globally from Global.asax:

    public static void RegisterGlobalFilters(GlobalFilterCollection filters) {
        // Make everything require authorization by default (whitelist approach)
        filters.Add(new AuthorizeSafeFilter());
    }

    public static void RegisterWebApiFilters(HttpFilterCollection filters) {
        // Make everything require authorization by default (whitelist approach)
        filters.Add(new AuthorizeSafeApiFilter());
    }

Then to open up an action to eg. anonymous access or only Admin access:

public class AccountController : System.Web.Mvc.Controller {
    // GET: /Account/Login
    [AllowAnonymous]
    public ActionResult Login(string returnUrl) {
        // ...
    }
}

public class TestApiController : System.Web.Http.ApiController {
    // GET API/TestApi
    [AuthorizeSafe(Roles="Admin")]
    public IEnumerable<TestModel> Get() {
        return new TestModel[] {
            new TestModel { TestId = 123, TestValue = "Model for ID 123" },
            new TestModel { TestId = 234, TestValue = "Model for ID 234" },
            new TestModel { TestId = 345, TestValue = "Model for ID 345" }
        };
    }
}
like image 164
Jez Avatar answered Oct 15 '22 08:10

Jez