ASP.NET MVC Forms authentication against external web service

Tom Haigh picture Tom Haigh · Apr 12, 2011 · Viewed 14.7k times · Source

I am trying to write an ASP.NET MVC application which is a frontend to our CRM which has a SOAP web service. I would like the user to log in to my web application using their CRM username and password, and then authenticate against CRM, make web service calls on the pages etc.

I started to look at using Forms Authentication and implementing a custom membership provider - I can implement all the methods I need to like ValidateUser(), but the problem I have is that after logging in to the CRM web service you are given a token which has to be passed with every subsequent web service call, and I am not sure where I can store this.

So my questions are:

  • is Forms Authentication the way to go here, or is it going to be more straightforward to handle all of the authentication myself and store the token in Session.
  • If Forms Authentication is the way to go, where and how should I store additional information like this. It seems likes using Forms Authentication but then ramming a load of additional information (which is related to authentication) into a cookie or session outside this would be a bit of a mess?

Any advice would be appreciated

Answer

Darin Dimitrov picture Darin Dimitrov · Apr 12, 2011

You can store the authentication token in the userData part of the forms authentication cookie. This way it will be available on each request.

So for example once you verify the credentials of a user you could query the web service to obtain the token and manually create and emit the forms authentication cookie:

[HttpPost]
public ActionResult LogOn(string username, string password)
{
    // TODO: verify username/password, obtain token, ...
    // and if everything is OK generate the authentication cookie like this:

    var authTicket = new FormsAuthenticationTicket(
        2,
        username,
        DateTime.Now,
        DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes),
        false,
        "some token that will be used to access the web service and that you have fetched"
    );
    var authCookie = new HttpCookie(
        FormsAuthentication.FormsCookieName, 
        FormsAuthentication.Encrypt(authTicket)
    )
    {
        HttpOnly = true
    };
    Response.AppendCookie(authCookie);

    // ... redirect
}

Then you could write a custom authorize attribute which will read this information and set a custom generic identity:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class MyAuthorizeAttribute : AuthorizeAttribute
{
    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        var isAuthenticated = base.AuthorizeCore(httpContext);
        if (isAuthenticated) 
        {
            string cookieName = FormsAuthentication.FormsCookieName;
            if (!httpContext.User.Identity.IsAuthenticated ||
                httpContext.Request.Cookies == null || 
                httpContext.Request.Cookies[cookieName] == null)
            {
                return false;
            }

            var authCookie = httpContext.Request.Cookies[cookieName];
            var authTicket = FormsAuthentication.Decrypt(authCookie.Value);

            // This is where you can read the userData part of the authentication
            // cookie and fetch the token
            string webServiceToken = authTicket.UserData;

            IPrincipal userPrincipal = ... create some custom implementation
                                           and store the web service token as property

            // Inject the custom principal in the HttpContext
            httpContext.User = userPrincipal;
        }
        return isAuthenticated;
    }
}

Finally decorate your controllers/actions that require authentication with this attribute:

[MyAuthorize]
public ActionResult Foo()
{
    // HttpContext.User will represent the custom principal you created
    // and it will contain the web service token that you could use to 
    // query the remote service
    ...
}