After, mostly, getting the SSO / OAuth2 server setup with OWIN working over the past week, I ran into a few interesting scenarios and bits of information worth sharing.
The main scenario was in dealing with Authorization Code Grant across different domains and working that scenario into both the SSO paradigm and “protected Resource” server access.
With my setup, I have one MVC5/WebApi application that is both the Secure Token endpoint and protected Resource manager. In this configuration, I have the expectation that, for example, a consuming application could utilize either cookie-based or bearer token based authentication. By default, OWIN will use the machine key for its cookie-based authentication encryption. So, any application that shares the machine key, cookie domain, and cookie name, would also be sharing the authentication provided by the SSO server.
I like this method, but it isn’t always feasible – especially if the consuming application isn’t .NET based. It also doesn’t work, obviously, if the applications are on different domains due to the way browser cookie security is managed. This is where our consuming application has to rely on tokens and protected resources.
Let’s say we configure our consuming application (via OWIN) like so:
public void Configuration(IAppBuilder app) { var appCookieOptions = new CookieAuthenticationOptions() { AuthenticationType = "CrossDomainApp", AuthenticationMode = AuthenticationMode.Active, LoginPath = new PathString("/Account/Login"), LogoutPath = new PathString("/Account/Logout"), CookieSecure = CookieSecureOption.Always, CookieName = "crazycookie", CookieDomain = "", CookiePath = "/", SlidingExpiration = true }; app.UseCookieAuthentication(appCookieOptions); }
These settings are deliberately unique to ensure that our Secure Token Service’s cookie authentication doesn’t match what we expect. Therefore, a user authenticated by the SSO login will not be authenticated, immediately, on our consuming site.
We would still define our login in the same manner as previously described, using DotNetOpenAuth in our AccountController. This method would make the initial code grant request.
private WebServerClient _webServerClient; public const string _authServer = "https://localhost:8888/"; public const string _authorizePath = "/OAuth/Authorize"; public const string _tokenPath = "/OAuth/Token"; public const string _loginPath = "/Account/Login"; public const string _logoutPath = "/Account/Logout"; [AllowAnonymous] public virtual ActionResult Login() { InitializeWebServerClient(); // Callback URI var returnUri = new Uri("https://localhost:9999/account/logincallback"); var userAuthorization = _webServerClient.PrepareRequestUserAuthorization(new[] { "identity", "roles" }, returnUri); userAuthorization.Send(HttpContext); Response.End(); return null; } private void InitializeWebServerClient() { var authorizationServerUri = new Uri(_authServer); var authorizationServer = new AuthorizationServerDescription { AuthorizationEndpoint = new Uri(authorizationServerUri, _authorizePath), TokenEndpoint = new Uri(authorizationServerUri, _tokenPath) }; _webServerClient = new WebServerClient(authorizationServer, "clientid", "clientsecret"); }
The big difference is in the LoginCallback. Within our login callback, since we’re using Authorization Code grant, we’ll get the code/state and then make the request for the access and refresh tokens.
That’s actually pretty easy with DNOA:
public virtual ActionResult LoginCallback() { InitializeWebServerClient(); var authorizationState = _webServerClient.ProcessUserAuthorization(Request); if (authorizationState != null) { var accessToken = authorizationState.AccessToken; var refreshToken = authorizationState.RefreshToken; } }
With just the token, and since we can’t decrypt it, this is where we have to rely on the protected Resource end-point to decrypt the token for us and let us know the information we may want about the user. In this case, we mainly just want the Claims to order to be able to persist them within our own domain’s auth cookie.
Our protected resource WebApi method (on our api/UserController) is pretty simple. OWIN will intercept the bearer token that is passed in, decrypt it, and attach the resolved Identity to the context. We can then utilize this information to send the user’s Roles/Claims/etc back to the caller:
public object Get() { var identity = this.User.Identity as ClaimsIdentity; var roleClaims = identity.Claims.Where(x => x.Type == ClaimsIdentity.DefaultRoleClaimType).Select(x => x.Value).ToList(); var nonRoleClaims = identity.Claims.Where(x => x.Type != ClaimsIdentity.DefaultRoleClaimType).Select(x => new { Type = x.Type, Value = x.Value }).ToList(); return new { name = identity.Name, roles = roleClaims, claims = nonRoleClaims }; }
In our LoginCallback, at this point we only need to make the call to the WebApi action and create our auth cookie based on the information returned. This is accomplished via HttpClient and attaching the bearer token to the headers:
public void CreateCookie(string accessToken) { using (var client = new HttpClient() { Timeout = TimeSpan.FromMilliseconds(10000) }) { var request = new HttpRequestMessage() { RequestUri = new Uri(string.Format("{0}/api/user", _authServer)), Method = HttpMethod.Get }; request.Headers.Add("Authorization", string.Format("Bearer {0}", accessToken)); HttpResponseMessage response = client.SendAsync(request).Result; Task<Stream> streamTask = response.Content.ReadAsStreamAsync(); Stream stream = streamTask.Result; var sr = new StreamReader(stream); var json = sr.ReadToEnd(); var converter = new ExpandoObjectConverter(); dynamic obj = JsonConvert.DeserializeObject<ExpandoObject>(json, converter); // Create the identity var authManger = HttpContext.GetOwinContext().Authentication; var identity = new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, obj.name) }, "CrossDomainApp"); foreach (var role in obj.roles) { identity.AddClaim(new Claim(ClaimsIdentity.DefaultRoleClaimType, role)); } // Create a delegate to filter the claims Func<dynamic, bool> matchClaimType = x => x.type != ClaimsIdentity.DefaultNameClaimType; // Add the other claims minus the name since we already added that foreach (var claim in System.Linq.Enumerable.Where<dynamic>(obj.claims, matchClaimType)) { identity.AddClaim(new Claim(claim.type, claim.value)); } authManger.SignIn(new AuthenticationProperties { IsPersistent = false }, identity); } // Alternatively, we could all the resource manager on the AuthServer to get user info. var cacheKey = string.Format("redirectUrl_{0}", User.Identity.Name); var redirectUrl = (string)HttpRuntime.Cache[cacheKey] ?? "/"; HttpRuntime.Cache.Remove(cacheKey); Response.Redirect(redirectUrl, false); return null; }
This bit of code makes the call and parses the JSON returned using JSON.NET and dynamics. After all of that information is rolled up into a ClaimsIdentity, we call OWIN’s AuthenticaitonManager to perform the signin. Based on our OWIN configuration, this is the point at which our auth cookie is created.
On the SSO server side, I did find that having the [Authorize] attribute overridden, as I previously mentioned, globally did cause problems. Doing this made it so that MVC endpoints were being authorized properly, but WebApi endpoints were not. This bit of code had to be refactored into custom MVC/WebApi filters. This is just something to be aware of if you happen to notice that unauthenticated users can suddenly access your API endpoints.
Another neat trick I stumbled upon is that if you, for whatever reason, didn’t want OWIN in the middle of your requests, you can actually decrypt the Bearer tokens yourself. The Microsoft.Owin.Security namespace has an IDataProtector interface that allows us to define how to decrypt. We can use implement this interface simply:
private ClaimsIdentity GetIdentityFromToken() { var token = this.Request.Headers.GetValues("Authorization").FirstOrDefault(x => x.ToLower().Contains("bearer")); var secureDataFormat = new TicketDataFormat(new MachineKeyProtector()); AuthenticationTicket ticket = secureDataFormat.Unprotect(token); var identity = ticket.Identity; return identity; } /// <summary> /// Helper method to decrypt the OWIN ticket /// </summary> private class MachineKeyProtector : IDataProtector { private readonly string[] _purpose = { typeof(OAuthAuthorizationServerMiddleware).Namespace, "Access_Token", "v1" }; public byte[] Protect(byte[] userData) { throw new NotImplementedException(); } public byte[] Unprotect(byte[] protectedData) { return System.Web.Security.MachineKey.Unprotect(protectedData, _purpose); } }
One other random find… On the OWIN/SSO server, by default, if a user isn’t authenticated, they will be redirected to the login page. For AJAX requests, I find this to be undesirable. My preference for AJAX requests is to return a 401/403 error rather than a redirect. In our cookie options, we can enforce this behavior pretty easily with OWIN’s CookieAuthenticationProvider:
var appCookieOptions = new CookieAuthenticationOptions() { AuthenticationType = CookieAuthenticationTypes.Application, AuthenticationMode = AuthenticationMode.Active, LoginPath = new PathString(AppSettings.LoginPath), LogoutPath = new PathString(AppSettings.LogoutPath) }; // We want to only redirect if the request is not an AJAX request on authorized appCookieOptions.Provider = new CookieAuthenticationProvider() { OnApplyRedirect = ctx => { if (!IsAjaxRequest(ctx.Request)) { ctx.Response.Redirect(ctx.RedirectUri); } } }; // Enable Application Sign In Cookie app.UseCookieAuthentication(appCookieOptions);
Looking at the code, the redirect is not made if the request was AJAX. And here’s our simple method to determine if the request was an AJAX request:
private static bool IsAjaxRequest(IOwinRequest request) { IReadableStringCollection query = request.Query; if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest")) { return true; } IHeaderDictionary headers = request.Headers; return ((headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest")); }
That’s it for this post. The more I use OWIN for various authentication/security scenarios, the more I appreciate it.
Nice article, can you provide sample code for this SSO?
Check out my entire series dealing with OWIN/OAuth. Generally speaking, all of the pieces are there to put together an OAuth (SSO) server using OWIN.
https://long2know.com/category/security/owin/
If you need help putting together a working project beyond that, I can help.
Okay thank you, i will check and if i need any help surely i will let you know.
Hi, Your articles are excellent focusing some real time problem scenarios and work around for those.
I appreciate if you can share code (project) for SSO for me to relate and understand better.
Regards – Jagan S
Could you please tell me can we implement SSO concept on client side?
Two Angular Applications have Login form
Request to WebAPI
WEBAPI return token and cookie
Then we utilize cookie or token for both applications, no need separate login.
Is it possible?
Generally speaking, for persistent authentication/authorization, you’re going to store a cookie on the user’s browser so that it’s persistent across requests. Bearer tokens are mostly used for accessing resources on behalf of a user. With that in mind, if both applications are on the same domain, the simplest thing to do is share the cookie.
This is basically what I do with multiple applications that are accessing a single login point. Since the cookie is shared, if the user logs in at one application, and then navigates to the other application, they are already logged in.
Hi Stephen,
Excellent articles about oauth. Had one query thought, might this site will be best place for asking.
We have implemented aouth in asp.net webapi using Microsoft.Owin.Security.OAuth. Now we have two asp.net webapi, where we need login with same Json Web Token.
For eg. if we login into one webapi and receive token, the same token should be valid for the other webapi. Like single sign on sort of, but i am unable to find proper place where to write custom logic for generating token and validating them.
Any help shall be appreciated.
Thanks Manoj.
If you’re using Microsoft.Owin.Security.OAuth without the .NET Core features, then as long as the machineKey in your web.config is shared between sites, the token can be decrypted/valid for every site involved.
I discuss this a fair bit in this article.
If you’re using the newer .NET Core key ring data protector, then you have to import the appropriate .NET Core data protection libraries and ensure that all involved applications utilize the same protection mechanisms. I’ve written a few posts about it here:
https://long2know.com/2017/05/sharing-cookies-and-tokens-between-owin-and-net-core/
https://long2know.com/2017/05/sharing-cookies-and-tokens-between-owin-and-net-core-part-2/