Question

I am attempting to re-do a login system using Claims-Based auth.

So far, so good

Stepping through, it appears to correctly evaluate a username and password and correctly create a claims principal (including adding an authentication type in order to set IsAuthenticated to true, per this SO question.)

But then...

Somehow the identity doesn't seem to correctly get set on the wire. As a result, I'm redirected directly back to the login page.

The Code

I have the following in global.asax:

private void Application_PostAuthenticateRequest(object sender, EventArgs e)
{
    var currentPrincipal = ClaimsPrincipal.Current; 
    var transformer = new ClaimsTransformer(); //My own custom transformer; code below.
    var newPrincipal = transformer.Authenticate(string.Empty, currentPrincipal); // does the transformation

    // as I understand, it is proper & recommnded to set both of these
    Thread.CurrentPrincipal = newPrincipal;
    HttpContext.Current.User = newPrincipal;
}

In my Login Controller, I have a simple test against a membership DB. I verified while debugging that this has newCP as a valid, authenticated identity that has the expected name.

[HttpPost]
[AllowAnonymous]
public ActionResult UserLogin(LoginViewModel viewModel)
{

    var loginSuccess = Membership.ValidateUser(viewModel.UserName, viewModel.Password);

    if (loginSuccess)
    {
        // CustomApplicationIdentity puts some identity-based logic into business domain terms and uses Claims underneath. 
        //Should have done it at the IPrincipal level, but instead I created the ToAuthenticatedStandardClaimsIdentity() which returns a new authenticated ClaimsIdentity.

        var newIdentity = new CustomApplicationIdentity(viewModel.UserName);
        var cp = new ClaimsPrincipal(newIdentity.ToAuthenticatedStandardClaimsIdentity());

        var newCP = new ClaimsTransformer().Authenticate(string.Empty, cp);
        System.Web.HttpContext.Current.User = newCP;
        Thread.CurrentPrincipal = newCP;

        if (!string.IsNullOrWhiteSpace(viewModel.ReturnUrl))
        {
            return Redirect(viewModel.ReturnUrl);
        }
        return RedirectToAction("Index", "Identity");

    }
}

The Problem

When it redirects to the Action, I see it hit the Application_PostAuthenticateRequest again, which makes perfect sense.

However, despite previously setting the principal, this now appears to be an empty principal (no name, with IsAuthenticated set to false).

Where am I Going Wrong?

Some thoughts:

  • Is it because I haven't set up the SessionSecurityToken yet?
  • Am I completely missing something regarding threading or setting the context correctly?
  • Since the UserLogin method is in MVC, I also tried using controller context, but that didn't seem to work either.
  • Is it possible that something else could be messing with this in the middle?
    • Read: Is there an easy way to verify that some portion of the old login system isn't left over and toying with me?
Was it helpful?

Solution

After a ton of research (and wading through the excellent Pluralsight Course by Dominick Baier), the solution was the following:

The Over-arching Big Steps / Problems

  • I wasn't setting a session authentication cookie, so the redirect was being treated as a new request, which saw no cookie and didn't set a principal.
  • Later, when I used a session authentication manager, it turns out that Cassini (VS's built in debug server) wasn't loading the SessionAuthenticationManager at all
    • (IIS and IIS Express do so just fine).

The Full Solution

Step by step (again, much of this is credited to Dominick's video):

Step 1: Add identity services to config

  • Right-click on your project, and select "Add Reference..."
  • In the Framework section, select System.IdentityModel.Services
  • Add the following to your web.config:

(full outline below, insert the two sections within that outline in your web.config):

<configuration>
    <configSections>
        <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
        <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    </configSections>
</configuration>

Step 2: Add session authentication manager

(which depends on that config setting)

In the system.webServer section of your web.config, add the following lines:

  <remove name="RoleManager"/> <!--Not needed anymore in my case -->
  <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>

Step 3: Remove the PostAuthenticate method in Global.asax

(no longer needed because of SAM, which detects the cookie; why run it on every request if you don't have to, right?)

Step 4: Set your Claims Transformation method to set the authentication cookie

Add these lines in your ClaimsAuthenticationManager (mine was called ClaimsTransformer). I put this in a separate method called "EstablishSession", which took in my principal after it was already transformed:

private void EstablishSession(ClaimsPrincipal transformedPrincipal)
{
    var sessionToken = new SessionSecurityToken(transformedPrincipal, TimeSpan.FromHours(8));
    FederatedAuthentication.SessionAuthenticationModule.WriteSessionTokenToCookie(sessionToken);
}

So now the cookie is always set whenever you transform a claim, which makes sense because you're only transforming a claim if the user was successfully authenticated.

Step 5: Tear your hair out for a bit...

...wondering why SessionAuthenticationManager is always null.

Seriously, everything seems to work, and your config is correct, but darn it if it isn't null every. single. time.

Step 6: Switch the debug web server to IIS Express

Ahhhh, it appears that Cassini (the build in VS Debugger) doesn't work with SessionAuthenticationManager.

However, IIS Express does. Switch it to that in your project settings.

And Voila!

Now I have a page that works.

OTHER TIPS

I think you need to implement the SessionSecurityToken, or something that will persist your session between page requests. Here is a more custom type of approach:

    public static int SetAuthCookie(this HttpResponseBase responseBase, User user, bool rememberMe)
    {
        // Initialize Session Ticket
        var authTicket = new FormsAuthenticationTicket(1
            , user.Email
            , DateTime.Now
            , DateTime.Now.AddHours(30)
            , rememberMe
            , JsonConvert.SerializeObject(new {
                Email = user.Email,
                FirstName = user.FirstName,
                Id = user.Id
            })
            , FormsAuthentication.FormsCookiePath);

        var encTicket = FormsAuthentication.Encrypt(authTicket);
        HttpCookie authCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);

        if (authTicket.IsPersistent)
            authCookie.Expires = authTicket.Expiration;

        responseBase.Cookies.Add(authCookie);
        return encTicket.Length;
    }



    public static void VerifyAuthCookie(HttpContext context)
    {

        HttpCookie authCookie = context.Request.Cookies[FormsAuthentication.FormsCookieName];
        if (authCookie == null)
            return;

        FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
        if (authTicket == null)
            return;

        if (authTicket.Expired)
            return;

        User user = !string.IsNullOrEmpty(authTicket.UserData) ? JsonConvert.DeserializeObject<User>(authTicket.UserData) : null;
        if (user == null)
            return;

        // Create an Identity object
        UserIdentity id = new UserIdentity(user, authTicket);

        // This principal will flow throughout the request.
        GenericPrincipal principal = new GenericPrincipal(id, new [] { "User" });
        context.User = principal;

}

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top