With a new project we have, I was tasked with working on security. Initially, I used OWIN and cookie authentication to implement a simple login and all was good. However, we wanted to remove the ability to login and have it driven by an external site redirecting a user with a token.
This is where the trouble began.
I created a simple AuthorizeAttribute which was used to decorate the primary MvcController’s Index (default) action. The process was pretty simple. Check to see if the user is authenticated, if they aren’t, then check for the prescence of a token. Validate the token. If the token is valid, call the OWIN authentication manager’s SignIn() method. Also, store the token as a claim and compare it to the token passed in. If the token doesn’t match, SignOut() the user and start the token validation/signin again.
Fortunately, with Owin, we can intercept most of the things the middleware does. For instance, we can see when a SignIn/SignOut/etc occur by providing handlers:
var appCookieOptions = new CookieAuthenticationOptions() { AuthenticationType = CookieAuthenticationTypes.Application, AuthenticationMode = AuthenticationMode.Active, LoginPath = new PathString(AppSettings.LoginPath), LogoutPath = new PathString(AppSettings.LogoutPath) }; appCookieOptions.Provider = new CookieAuthenticationProvider() { OnResponseSignIn = ctx => { // This method was NOT being called }, OnResponseSignOut = ctx => { // We can log the sign out here if we want }, OnValidateIdentity = async ctx => { // We can manipulate the identity here if we want }, OnApplyRedirect = ctx => { if (!IsAjaxRequest(ctx.Request)) { ctx.Response.Redirect(ctx.RedirectUri); } }, OnException = ctx => { throw ctx.Exception; } };
Within the authorize attribute, I retrieved the OwinContext like I always did:
public IAuthenticationManager AuthManager { get { if (_authManager == null) _authManager = HttpContext.Current.GetOwinContext().Authentication; return _authManager; } set { _authManager = value; } }
This would work on first login (token present). However, if I forced a SignOut, and returned to the site with the same, valid token, the user would not be logged in. Some digging revealed that the authentication cookie was not being attached to the response. The “OnResponseSignIn” would only get called on initial validation. Why was this happening though? I suspect that it was the merging of old and new technologies where the older .NET pipeline/context were not behaving as expected due to where we were in the authentication flow.
Within the AuthorizeAttribute’s OnAuthorization method, we have a handle to the AuthorizationContext. This context has a handle to the HttpContext. Making one change to get the OwinContext from the AuthorizationContext seemed to fix the problem.
public override void OnAuthorization(AuthorizationContext filterContext) { _authManager = filterContext.HttpContext.GetOwinContext().Authentication; _authManager.SignIn(...); }
Without being able to observe the entire middleware flow, thanks to Owin’s interface, this problem would have probably eluded me for longer than it did. It was also partially a random chance that I tried retrieving the OwinContext from the AuthorizationContext rather than System.Web.HttpContext.Current. It was partially an educated guess because I had previously read about issues in the way the older, rigid System.Web namespaces worked with the newer System.Net (and subsequently Owin middleware) namespaces. In some cases, the older pieces of the .NET framework can play havoc on the CookieCollection provided on the response. I’m still speculating at the reasons, but proof is in the empirical observation.
The bottom line, it seems, is that context and process flow are tantamount to a fully working security mechanism.
On a side note, Owin also provides ways to intercept the entire process of Appending/Deleting cookies from the response via the ICookieManager interface. This is one of the things that lead me to my (partially) educated guess in dealing with System.Web. We can use an ICookieManager and ensures that System.Web doesn’t overwrite our authentication cookies. We only have to provide the manager and tell Owin to use it.
appCookieOptions.CookieManager = new SystemWebCookieManager(); public class SystemWebCookieManager : ICookieManager { public string GetRequestCookie(IOwinContext context, string key) { if (context == null) { throw new ArgumentNullException("context"); } var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName); var cookie = webContext.Request.Cookies[key]; return cookie == null ? null : cookie.Value; } public void AppendResponseCookie(IOwinContext context, string key, string value, CookieOptions options) { if (context == null) { throw new ArgumentNullException("context"); } if (options == null) { throw new ArgumentNullException("options"); } var webContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName); bool domainHasValue = !string.IsNullOrEmpty(options.Domain); bool pathHasValue = !string.IsNullOrEmpty(options.Path); bool expiresHasValue = options.Expires.HasValue; var cookie = new HttpCookie(key, value); if (domainHasValue) { cookie.Domain = options.Domain; } if (pathHasValue) { cookie.Path = options.Path; } if (expiresHasValue) { cookie.Expires = options.Expires.Value; } if (options.Secure) { cookie.Secure = true; } if (options.HttpOnly) { cookie.HttpOnly = true; } webContext.Response.AppendCookie(cookie); } public void DeleteCookie(IOwinContext context, string key, CookieOptions options) { if (context == null) { throw new ArgumentNullException("context"); } if (options == null) { throw new ArgumentNullException("options"); } AppendResponseCookie( context, key, string.Empty, new CookieOptions { Path = options.Path, Domain = options.Domain, Expires = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc), }); } }
Can you provide a full project – External site (sso) & integration site
Using OWIN?