11
votes

I have a webforms website that is calling into a new MVC6 website that we are working on. The user will login as they always have done on the webforms website using forms authentication and then get redirected to the new MVC6 website. I know in MVC6 that I should be using Cookie Authentication but cannot get it to decrypt the cookie. I suspect its down to changes around web.config and machinekey but am really stuck.

Here is what I have done.

I have set up cookie authentication as follows

        app.UseCookieAuthentication(options =>
        {
            options.CookieName = "MyWebformsCookie";
            options.AutomaticAuthenticate = true;
            options.AuthenticationScheme = "Cookies";
            options.TicketDataFormat = new MySecureDataFormat();
            options.DataProtectionProvider = new MyDataProtectionProvider();
            //options.CookieDomain = "localhost";
        });

The class is as follows

public class MySecureDataFormat : ISecureDataFormat<AuthenticationTicket>
{
    public string Protect(AuthenticationTicket data)
    {
        return string.Empty;
    }

    public string Protect(AuthenticationTicket data, string purpose)
    {
        return string.Empty;
    }

    public AuthenticationTicket Unprotect(string protectedText)
    {
        return null;
    }

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        var ticket = FormsAuthentication.Decrypt(protectedText);
        return null;
    }
}

The cookie is being read, and the Unprotect method called, but then it errors on the FormsAuthentication.Decrypt method with error

An exception of type 'System.Web.HttpException' occurred in System.Web.dll but was not handled in user code

Additional information: Unable to validate data.

Stack = at System.Web.Configuration.MachineKeySection.EncryptOrDecryptData(Boolean fEncrypt, Byte[] buf, Byte[] modifier, Int32 start, Int32 length, Boolean useValidationSymAlgo, Boolean useLegacyMode, IVType ivType, Boolean signData) at System.Web.Security.FormsAuthentication.Decrypt(String encryptedTicket) at WebApplication.Mvc.MySecureDataFormat.Unprotect(String protectedText, String purpose) in C:\SVNCode\GlobalConnectV2\WebApplication.Mvc\Startup.cs:line 153
at Microsoft.AspNet.Authentication.Cookies.CookieAuthenticationHandler.d__9.MoveNext()

So this leads me to believe that its not reading machine key. I have this in the web.config in wwwroot folder

<configuration>
  <system.webServer>
    ...
  </system.webServer>
  <system.web>
    <machineKey compatibilityMode="Framework20SP2" validation="SHA1" decryption="AES" validationKey="mykey" decryptionKey="dec" />
  </system.web>
</configuration>

This works on earlier MVC apps but guessing something changed in MVC6. I have also tried the following but no luck

    services.ConfigureDataProtection(configure =>
    {
        configure.UseCryptographicAlgorithms(new Microsoft.AspNet.DataProtection.AuthenticatedEncryption.AuthenticatedEncryptionOptions()
        {
            EncryptionAlgorithm = Microsoft.AspNet.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm.AES_256_CBC,
            ValidationAlgorithm = Microsoft.AspNet.DataProtection.AuthenticatedEncryption.ValidationAlgorithm.HMACSHA256
        });

    });

Any advice?

3

3 Answers

13
votes

I had no joy attempting to use FormsAuthentication.Decrypt() in an ASP.NET 5 application.

In the end I wrote a decryption routine, based on the documentation available, and also looking at reference source code that Microsoft made available for system web.

The classes required to decrypt a forms authentication cookie that uses SHA1 for validation, and AES for encryption, can be found on my GIST here: https://gist.github.com/dazinator/0cdb8e1fbf81d3ed5d44

Once you have these, you can create a custom TicketFormat as before:

public class FormsAuthCookieTicketFormat : ISecureDataFormat<AuthenticationTicket>
{

    private LegacyFormsAuthenticationTicketEncryptor _Encryptor;
    private Sha1HashProvider _HashProvider;

    public FormsAuthCookieTicketFormat(string decryptionKey, string validationKey)
    {
        _Encryptor = new LegacyFormsAuthenticationTicketEncryptor(decryptionKey);
        _HashProvider = new Sha1HashProvider(validationKey);
    }

    public string Protect(AuthenticationTicket data)
    {
        throw new NotImplementedException();            
    }

    public string Protect(AuthenticationTicket data, string purpose)
    {
        throw new NotImplementedException();
    }

    public AuthenticationTicket Unprotect(string protectedText)
    {
        throw new NotImplementedException();
    }

    public AuthenticationTicket Unprotect(string protectedText, string purpose)
    {
        var ticket = _Encryptor.DecryptCookie(protectedText, _HashProvider);

        var identity = new ClaimsIdentity("MyCookie");
        identity.AddClaim(new Claim(ClaimTypes.Name, ticket.Name));
        identity.AddClaim(new Claim(ClaimTypes.IsPersistent, ticket.IsPersistent.ToString()));
        identity.AddClaim(new Claim(ClaimTypes.Expired, ticket.Expired.ToString()));
        identity.AddClaim(new Claim(ClaimTypes.Expiration, ticket.Expiration.ToString()));
        identity.AddClaim(new Claim(ClaimTypes.CookiePath, ticket.CookiePath));
        identity.AddClaim(new Claim(ClaimTypes.Version, ticket.Version.ToString()));           

        // Add some additional properties to the authentication ticket.
        var props = new AuthenticationProperties();
        props.ExpiresUtc = ticket.Expiration.ToUniversalTime();
        props.IsPersistent = ticket.IsPersistent;

        var principal = new ClaimsPrincipal(identity);

        var authTicket = new AuthenticationTicket(principal, props, CookieDetails.AuthenticationScheme);
        return authTicket;
    }

And wire it up like so:

var formsCookieFormat = new FormsAuthCookieTicketFormat(_DecryptionKeyText, _ValidationKeyText);

        app.UseCookieAuthentication(options =>
        {
            // shortened for brevity... 
            options.TicketDataFormat = formsCookieFormat ;
            options.CookieName = "MyCookie";                
        });
2
votes

There is a whole section of documentation on docs.asp.net that is named Replacing machineKey.

It mostly has to do with the package Microsoft.AspNet.DataProtection.SystemWeb.

You will then be able to setup your key either in code or in config and be able to read those cookies directly from .

Here's a code sample (from the documentation) on how to do it:

public override void ConfigureServices(IServiceCollection services)
{
    services.ConfigureDataProtection(configure =>
    {
        configure.SetApplicationName("my-app");
        configure.PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\myapp-keys\"));
        configure.ProtectKeysWithDpapi();
    });
}

Of course you have to add the previously mentioned package in your list of dependencies and that may force you to use Windows (not sure but it does compile in CoreClr).

This will NOT work with existing machineKey but it will generate a new set that will allow you to start from there.

1
votes

When you authenticate the user, set the authentication cookie's domain to the second-level domain, i.e. parent.com (your asp.net side). Each sub-domain (MVC site) will receive the parent domain's cookies on request, so authentication over each is possible since you will have a shared authentication cookie to work with.

Authentication code:

System.Web.HttpCookie authcookie = System.Web.Security.FormsAuthentication.GetAuthCookie(UserName, False);
authcookie.Domain = "parent.com";
HttpResponse.AppendCookie(authcookie);
HttpResponse.Redirect(System.Web.Security.FormsAuthentication.GetRedirectUrl(UserName, 
                                                                   False));

Refer question Forms Authentication across Sub-Domains

and ASP.NET webforms and MVC authentication sharing via cookie