It seems like only yesterday when I setup an OWIN OAuth server to provide single-signon capabilities for all of my apps. Since that time, though, OWIN has kind of fallen to the wayside in favor of newer security mechanisms in .NET Core. However, it is possible to make an OWIN application play nice with a .NET Core application to share cookie-based authentication.
Token generation in OWIN is relatively easy to set-up. The encryption mechanisms (DataProtectors) will utilize the machine key in your web.config to provide all encryption. Cookies are no different.
Who moved my cheese?
With .NET Core, though, the machine key has gone the way of the Dodo and the concept of a static key for all encryption is kind of gone too. In its place, we now have a “key ring” concept by which .NET Core will generate new keys for encryption as needed. That is to say, encryption keys now are intended to be short-lived. This does present a problem in terms of shared keys across web farms, but it just means we have to have a way to share keys.
On top of the encryption key changes, the ticket format used for cookies is different. This means that even if we could encrypt things in OWIN the exact same way as .NET Core, our Tickets, which are used in Cookies and such, that are generated by OWIN, would still be unreadable by a .NET Core application.
Enter Microsoft.Owin.Security.Interop.
Microsoft introduced the Microsoft.Owin.Security.Interop library to help bridge the gap between OWIN security and .NET Core security. It provides the necessary classes to allow us to use the newer .NET Core DataProtectors for OWIN Cookie/Ticket encryption as well as Bearer, Refresh, and AccessCode token encryption. After pulling in this package from Nuget, the fun begins.
In our OWIN authorization server – the .NET 4.6.x web application with which we’re using IAppBuilder “UseOAuthAuthorizationServer” – a number of modifications must be made. The Cookie Middleware and the OAuth middleware must have their IDataProtectors set and the TicketDataFormat must be set. Additionally, for simplicity’s sake, I’m letting the authorization server create a static key that I can simply put into each project. In a real-world example, you’d want keys to be shared either through a database, repository, or Windows file share.
Jumping right in, in our Startup class, the encryption settings must be set and then used to define the IDataProtector. We’ll tackle our Cookie settings first.
// Changes to ticket data format to make OWIN compatible with netcore var encryptionSettings = new AuthenticatedEncryptionSettings() { EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm = ValidationAlgorithm.HMACSHA256 }; var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo($@"{HostingEnvironment.MapPath("~")}\keys"), options => { options.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20)); options.UseCryptographicAlgorithms(encryptionSettings);
On first run, I comment this line so that I get a single key created. I then use that key in other applications.
// Remove the keys and comment this line to create new keys options.DisableAutomaticKeyGeneration(); }); var dataProtector = protectionProvider.CreateProtector( "CookieAuthenticationMiddleware", "Cookie", "v2");
After defining the encrypting settings and using them to create an IDataProtector, we have to specify our TicketDataFormat in the CookieAuthenticationOptions. You’ll see that it’s using the AspNetTicketDataFormat with the DataProtectorShim. Other points here are that hte CookieName, Path, Domain, etc must be accessible/known by the client application that wants to share the Cookie.
var appCookieOptions = new CookieAuthenticationOptions() { AuthenticationType = CookieAuthenticationTypes.Application, AuthenticationMode = AuthenticationMode.Active, LoginPath = new PathString(AppSettings.LoginPath), LogoutPath = new PathString(AppSettings.LogoutPath), CookieName = ".AspNet.Cookie", CookieDomain = string.Empty, // for localhost CookiePath = "/" CookieSecure = CookieSecureOption.Always, ExpireTimeSpan = TimeSpan.FromMinutes(20), SlidingExpiration = true, TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)) }; app.UseCookieAuthentication(appCookieOptions); app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationTypes.Application);
Next, we can create DataProtectors for our Tokens.
var bearerProtector = protectionProvider.CreateProtector( "TokenMiddleware", "AccessToken", "v2"); var refreshProtector = protectionProvider.CreateProtector( "TokenMiddleware", "RefreshToken", "v2"); var accessCodeProtector = protectionProvider.CreateProtector( "TokenMiddleware", "AccessCode", "v2");
Those IDataProtectors are set in the OAuthAuthorizationServerOptions using the same DataProtectorShim. We must specify these in our bearer authorization as well as our Oauth Server.
// Set the ticket format for bearer tokens on requests app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions() { AccessTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(accessTokenProtector)) }); // Setup Authorization Server app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions { AuthorizeEndpointPath = new PathString(Paths.AuthorizePath), TokenEndpointPath = new PathString(Paths.TokenPath), ApplicationCanDisplayErrors = true, // Authorization server provider which controls the lifecycle of Authorization Server Provider = _authServerProvider, // Authorization code provider which creates and receives authorization code AuthorizationCodeProvider = _authCodeProvider, // Refresh token provider which creates and receives referesh token RefreshTokenProvider = _refreshTokenProvider, AccessTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(bearerProtector)), AuthorizationCodeFormat = new AspNetTicketDataFormat(new DataProtectorShim(refreshProtector)), RefreshTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(accessCodeProtector)) });
That takes care of the OWIN Authorization Server. It should not be using the updated encryption methodologies and should use the new ticket format. Any references to the default MachineKeyProtector should be removed since our machine key is no longer going to be used for encryption or decryption. Our client .NET 4.6.x application has to be updated in a similar fashion in order to ensure that it can digest the cookies. This includes adding the Microsoft.Owin.Security.Interop package and creating the CookieAuthenticationOptions in the same way as the OWIN Authorization Server.
On a side note, if you’re using Ninject, I defining the IDataProtectionProvider as a constant so that it can be injected if you have a use for it outside of the OWIN flow. For example, if I wanted to get the Bearer token from the Cookie and then decrypt it, I could do so in a similar way as could be previously done with a MachineKeyProtector after injecting IDataProctionProvider (as _protectionProvider):
private AuthenticationTicket GetAuthenticationTicketFromCookie() { var cookieName = ".AspNet.Cookie"; var cookie = HttpContext.Current.Request.Cookies.Get(cookieName); var ticket = cookie.Value; // Deal with uuencoding ticket = ticket.Replace('-', '+').Replace('_', '/'); var padding = 3 - ((ticket.Length + 3) % 4); if (padding != 0) { ticket = ticket + new string('=', padding); } var dataProtector = _protectionProvider.CreateProtector("CookieAuthenticationMiddleware", "Cookie", "v2"); var secureDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)); return secureDataFormat.Unprotect(ticket); } private string GetBearerTokenFromCookie() { var ticket = GetAuthenticationTicketFromCookie(); var dataProtector = _protectionProvider.CreateProtector("TokenMiddleware", "AccessToken", "v2"); var secureDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector)); var encryptedToken = secureDataFormat.Protect(ticket); return string.Format("Bearer {0}", encryptedToken); }
With these changes, our .NET 4.x OWIN based apps will all work happily together. To get the .NET Core application working in this happy workflow, we have to add the similar DataProtectors.
.NET Core Changes
Within the Startup.cs’s ConfigureServices, we will use the build IServiceCollection extensions to add data protection. Afterward, we will be able to inject the IDataProtectionProvider in a similar way as above. Optionally, you could create your IDataProtectionProvider and set it within a singleton scope.
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { var encryptionSettings = new AuthenticatedEncryptionSettings() { EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC, ValidationAlgorithm = ValidationAlgorithm.HMACSHA256 }; services .AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo($@"{_hostingEnv.ContentRootPath}\keys")) .SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20)) .DisableAutomaticKeyGeneration() .UseCryptographicAlgorithms(encryptionSettings);
In the Configure method of the Startup, the Ticket format has to be specified for the cookie. Note that the cookie name, domain, and path must match what we configured in OWIN.
// Add cookie middleware var protectionProvider = ServiceProviderFactory.ServiceProvider.GetService<IDataProtectionProvider>(); var dataProtector = protectionProvider.CreateProtector("CookieAuthenticationMiddleware", "Cookie", "v2"); app.UseCookieAuthentication(new CookieAuthenticationOptions { AutomaticAuthenticate = true, AutomaticChallenge = true, LoginPath = new PathString(oauthSettings.LoginPath), LogoutPath = new PathString(oauthSettings.LogoutPath), CookieName = ".AspNet.Cookie", CookieDomain = string.Empty, // for localhost CookiePath = "/" ExpireTimeSpan = TimeSpan.FromMinutes(20), SlidingExpiration = true, Events = new CookieAuthenticationEvents() { OnValidatePrincipal = async context => { await ValidateAsync(context); } }, TicketDataFormat = new TicketDataFormat(dataProtector) });
With these pieces in place, our Cookies should be shared and work with our OWIN based and .NET Core based applications. Of course, the static key should be replaced with a real shared key mechanism, such as a database repository, but sharing a single static key was a more direct way for me to confirm things were working.
Also worth mentioning, while one would be able to decrypt Bearer tokens across applications, in .NET Core, an ISecureDataFormat implementation must be passed in the OAuth settings to actually perform the protect/unprotect routines unlike OWIN which will handle things itself by simple knowing what the data protectors are.
Hello,
I try to use
var appCookieOptions = new CookieAuthenticationOptions()
{
AuthenticationType = CookieAuthenticationTypes.Application,
But CookieAuthenticationTypes.Application is not exist.
can you give me NuGet url or Assembly link ?
Thanks 🙂
For pure OWIN, I believe it’s here:
https://www.nuget.org/packages/Microsoft.Owin.Security.Cookies/
The .NET Core equivalent (without OWIN) is here:
https://www.nuget.org/packages/Microsoft.Owin.Security.Cookies/
But, I think that CookieAuthenticationOptions is also included in those Nuget libraries. Does CookieAuthenticationOptions exist?
Hi,
In fact my problem has not coming from AuthenticationType = CookieAuthenticationTypes.Application.
For now, my code:
var appCookieOptions = new CookieAuthenticationOptions()
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
AuthenticationMode = AuthenticationMode.Active,
LoginPath = new PathString(Paths.LoginPath),
LogoutPath = new PathString(Paths.LogoutPath),
CookieName = “MySession”,
CookieDomain = string.Empty, // for localhost
CookiePath = “/”,
//CookieSecure = CookieSecureOption.Always,
ExpireTimeSpan = TimeSpan.FromMinutes(20),
SlidingExpiration = true,
TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector))
};
==> I have comment CookieSecure = CookieSecureOption.Always,
I’m in http for my dev 😉
I have a RessourceServer that was using machinekey et OAuth token , i have remove machinekey from my web.config and adapt my code:
public void ConfigureAuth(IAppBuilder app)
{
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
//app.UseOAuthBearerAuthentication(new Microsoft.Owin.Security.OAuth.OAuthBearerAuthenticationOptions());
// Set the ticket format for bearer tokens on requests
var encryptionSettings = new AuthenticatedEncryptionSettings()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
};
var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@”c:\temp”), options =>
{
options.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20));
options.UseCryptographicAlgorithms(encryptionSettings);
// Remove the keys and comment this line to create new keys
options.DisableAutomaticKeyGeneration();
});
var accessTokenProtector = protectionProvider.CreateProtector(“CookieAuthenticationMiddleware”, “Cookie”, “v2”);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
AccessTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(accessTokenProtector))
});
}
By it doesn’t work, any idea ?
Thanks
Do you have a sample app ?
It will be more simple.
Thanks
Hi,
I have solded a part of my problem.
My 4.6.1 client application work with:
var encryptionSettings = new AuthenticatedEncryptionSettings()
{
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
};
var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo(@”c:\temp”), options =>
{
options.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20));
options.UseCryptographicAlgorithms(encryptionSettings);
// Remove the keys and comment this line to create new keys
options.DisableAutomaticKeyGeneration();
});
var accessTokenProtector = protectionProvider.CreateProtector(“TokenMiddleware”, “AccessToken”, “v2”);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()
{
AccessTokenFormat = new AspNetTicketDataFormat(new DataProtectorShim(accessTokenProtector)),
});
But with my core app in 1.1.1
It work but not with injection:
var protectionProvider = ServiceProviderFactory.ServiceProvider.GetService();
It produce an Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector instance
It work with:
var protectionProvider = DataProtectionProvider.Create(new DirectoryInfo($@”c:\temp”), options =>
{
options.SetDefaultKeyLifetime(TimeSpan.FromDays(365 * 20));
options.UseCryptographicAlgorithms(encryptionSettings);
// Remove the keys and comment this line to create new keys
options.DisableAutomaticKeyGeneration();
});
===> It produce an Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtectionProvider instance
Any idea ?
I do have some production instances of this scenario with an OWIN SSO server and .NET Core 1.1.2 / 2.0 preview client and everything works as expected. I’ll see if I can put a client/server into the same solution this afternoon and upload it to github.
Hi! I have the added challenge of my situation being the reverse of yours. I have a OWIN client application and a .NET Core Identity Server. I’ve mostly figured out how to switch your example around to get it to work for me. If I log into my .NET Core Identity Server, the cookie it creates is recognized by my OWIN client app. However, the reverse doesn’t work. If I log into my OWIN client, this cookie it creates isn’t recognized by my .NET Core Identity Server. The asymmetry doesn’t make any sense! Any ideas?
Nevermind. I figured it out. I had to do with the signing of the JWT. My Identity Server requires JWTs to be signed by a specific X509 cert and the client app wasn’t doing that.
Sorry for the slow reply – glad you got it working. That’s an interesting scenario to work through.
It would be sooooo awesome if you created a sample app and uploaded it to github. So, so, soooo awesome. Thanks
That is something I’ve been meaning to get around to for quite some time (since last year). No promises, but I’ll see if I can find time this weekend to share a fully working example.
Not able to reference to AuthenticatedEncryptionSettings.
I have commented that line out. However, I still getting an Unauthorized error on my client application (which is running on 4.6.1).
I’m getting 302 found after I inserted this appCookieOptions.
var appCookieOptions = new CookieAuthenticationOptions()
{
AuthenticationType = “Cookies”,
AuthenticationMode = Microsoft.Owin.Security.AuthenticationMode.Active,
LoginPath = new PathString(“/token”),
//LogoutPath = new PathString(AppSettings.LogoutPath),
CookieName = “.AspNet.Cookie”,
CookieDomain = string.Empty, // for localhost
CookiePath = “/”,
CookieSecure = CookieSecureOption.Always,
ExpireTimeSpan = TimeSpan.FromMinutes(20),
SlidingExpiration = true,
TicketDataFormat = new AspNetTicketDataFormat(new DataProtectorShim(dataProtector))
};
app.UseCookieAuthentication(appCookieOptions);