Beginning of this year, I wrote about how to make ClaimsIdentity work with Sitecore, after that I tried integrating Sitecore extranet authentication with OpenId Connect but had little trouble as I was using Owin based pipelines to perform the integration which obviously doesn’t work due to execution sequence of Sitecore processing.
Recently, I had a chance to look at it again and it turned out that it is much simpler to implement via plain code than Owin based OpenId Connect client, below steps talks about, how it can be achieved.
For implementation, you would need two application,
- Authentication Server
- Client
Authentication Server
I am using IdentityServer V3 as server to perform the authentication but it should work with any other provider without any issue. I will skip the server setup process as their documentation does that best than me, it’s available here. For this integration, I have configured a client in IdentityServer with following code.
public static class Clients { public static IEnumerable<Client> Get() { return new[] { new Client { Enabled = true, ClientName = "Sitecore Client", ClientId = "sitecore", Flow = Flows.Implicit, RedirectUris = new List<string> { "https://local-sitecore.com/SignInCallback" }, AllowAccessToAllScopes = true }, }; } }
If you look at the above code, it is using implicit flow and configured client name is “sitecore” with redirect url is – https://local-sitecore.com/SignInCallback, this is the page where IdentityServer will redirect after successful login.
My IdentityServer is hosted under following url – https://local-sts.com/identity
Client
My client is a Sitecore application primarily developed in Mvc with controller rendering, I am skipping the Sitecore page design aspect as they are pretty standard as like any other controller rendering.
But before we proceed, there are some prerequisites for this integration.
Prerequisite
You would need an Authentication Provider and Authentication Helper which understand Claims on the similar line of Sitecore’s Membership Provider.
This implementation and relevant code is available here – https://cprakash.com/2015/02/02/sitecore-with-claimsidentity/
Implementation
The client implementation consist of following
- SignIn method- SignIn method is responsible to redirect the user to Authentication server to provider the user id and password and validate the credential.
- SignIn Callback method- Callback method is responsible to accept the request after successful login and validate & process the id token provided to get the user claims. Once client has access to user claims, it can create authentication cookie with security token.
Most of the code is already available as part of the IdentityServer Samples, for this implementation you need to look for specific example given here.
Code
public class AccountController : Controller { public ActionResult SignIn() { var state = Guid.NewGuid().ToString("N"); var nonce = Guid.NewGuid().ToString("N"); var url = Constants.AuthorizeEndpoint + "?client_id=sitecore" + "&response_type=id_token" + "&scope=openid email" + "&redirect_uri=https://local-sitecore.com/SignInCallback" + "&response_mode=form_post" + "&state=" + state + "&nonce=" + nonce; SetTempCookie(state, nonce); return Redirect(url); } [HttpPost] public ActionResult SignInCallback() { var token = Request.Form["id_token"]; var state = Request.Form["state"]; var claims = ValidateIdentityToken(token, state); // Try login as Claim User SessionSecurityToken sessionToken; if (!FederatedAuthentication.SessionAuthenticationModule.TryReadSessionTokenFromCookie(out sessionToken)) { var nameClaim = claims.Where(c => c.Type == ClaimTypes.Name).FirstOrDefault(); if (nameClaim == null) nameClaim = claims.Where(c => c.Type == ClaimTypes.NameIdentifier).FirstOrDefault(); if (nameClaim != null) { var tempClaims = claims.Where(c => c.Type != ClaimTypes.Name); claims = (new Claim[] { new Claim(ClaimTypes.Name, Globalize(Context.Domain.Name, nameClaim.Value)) }).Concat(tempClaims); } var identity = new ClaimsIdentity(claims, "Forms", ClaimTypes.Name, ClaimTypes.Role); var principal = new ClaimsPrincipal(identity); var sessionSecurityToken = new SessionSecurityToken(principal); var sam = FederatedAuthentication.SessionAuthenticationModule; sam.WriteSessionTokenToCookie(sessionSecurityToken); } return Redirect("/Welcome"); } private IEnumerable<Claim> ValidateIdentityToken(string token, string state) { var certString = "MIIDBTCCAfGgAwIBAgIQNQb+T2ncIrNA6cKvUA1GWTAJBgUrDgMCHQUAMBIxEDAOBgNVBAMTB0RldlJvb3QwHhcNMTAwMTIwMjIwMDAwWhcNMjAwMTIwMjIwMDAwWjAVMRMwEQYDVQQDEwppZHNydjN0ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqnTksBdxOiOlsmRNd+mMS2M3o1IDpK4uAr0T4/YqO3zYHAGAWTwsq4ms+NWynqY5HaB4EThNxuq2GWC5JKpO1YirOrwS97B5x9LJyHXPsdJcSikEI9BxOkl6WLQ0UzPxHdYTLpR4/O+0ILAlXw8NU4+jB4AP8Sn9YGYJ5w0fLw5YmWioXeWvocz1wHrZdJPxS8XnqHXwMUozVzQj+x6daOv5FmrHU1r9/bbp0a1GLv4BbTtSh4kMyz1hXylho0EvPg5p9YIKStbNAW9eNWvv5R8HN7PPei21AsUqxekK0oW9jnEdHewckToX7x5zULWKwwZIksll0XnVczVgy7fCFwIDAQABo1wwWjATBgNVHSUEDDAKBggrBgEFBQcDATBDBgNVHQEEPDA6gBDSFgDaV+Q2d2191r6A38tBoRQwEjEQMA4GA1UEAxMHRGV2Um9vdIIQLFk7exPNg41NRNaeNu0I9jAJBgUrDgMCHQUAA4IBAQBUnMSZxY5xosMEW6Mz4WEAjNoNv2QvqNmk23RMZGMgr516ROeWS5D3RlTNyU8FkstNCC4maDM3E0Bi4bbzW3AwrpbluqtcyMN3Pivqdxx+zKWKiORJqqLIvN8CT1fVPxxXb/e9GOdaR8eXSmB0PgNUhM4IjgNkwBbvWC9F/lzvwjlQgciR7d4GfXPYsE1vf8tmdQaY8/PtdAkExmbrb9MihdggSoGXlELrPA91Yce+fiRcKY3rQlNWVd4DOoJ/cPXsXwry8pWjNCo5JD8Q+RQ5yZEy7YPoifwemLhTdsBz3hlZr28oCGJ3kbnpW0xGvQb3VHSTVVbeei0CfXoW6iz1"; var cert = new X509Certificate2(System.Convert.FromBase64String(certString)); var result = Request.Cookies["TempCookie"]; if (result == null) { throw new InvalidOperationException("No temp cookie"); } if (string.IsNullOrWhiteSpace(result.Values["state"])) { throw new InvalidOperationException("invalid state"); } var parameters = new TokenValidationParameters { ValidAudience = "sitecore", ValidIssuer = Constants.BaseAddress, IssuerSigningToken = new X509SecurityToken(cert) }; var handler = new JwtSecurityTokenHandler(); SecurityToken jwt; var id = handler.ValidateToken(token, parameters, out jwt); if (id.FindFirst("nonce").Value != result.Values["nonce"]) { throw new InvalidOperationException("Invalid nonce"); } this.HttpContext.Response.Cookies.Remove("TempCookie"); return id.Claims; } public ActionResult SignOut() { AuthenticationManager.Logout(); return Redirect(Constants.LogoutEndpoint); } private void SetTempCookie(string state, string nonce) { var cookie = new HttpCookie("TempCookie"); var tempId = new System.Collections.Specialized.NameValueCollection(); cookie.Values.Add("state", state); cookie.Values.Add("nonce", nonce); this.HttpContext.Response.Cookies.Add(cookie); } private static string Globalize(string domainName, string userName) { var str = userName; if (!userName.StartsWith(domainName + "\\")) str = domainName + "\\" + userName; return str; } } public static class Constants { public const string BaseAddress = "https://local-sts.com/identity"; public const string AuthorizeEndpoint = BaseAddress + "/connect/authorize"; public const string LogoutEndpoint = BaseAddress + "/connect/endsession"; public const string TokenEndpoint = BaseAddress + "/connect/token"; public const string UserInfoEndpoint = BaseAddress + "/connect/userinfo"; public const string IdentityTokenValidationEndpoint = BaseAddress + "/connect/identitytokenvalidation"; public const string TokenRevocationEndpoint = BaseAddress + "/connect/revocation"; public const string AspNetWebApiSampleApi = "https://local-sitecore.com/"; }
Above code deviates from original code to make it working with Sitecore. Original code drops the temporary cookie for state and none validation with Owin based methods to encrypt the values and create cookie but above code uses plain cookie to do that, you can use FormAuthentication’s encrypt method to perform the encryption.
Original code uses ClaimTypes.NameIdentifier but I have to add ClaimTypes.Name manually so that SitecoreIdentity can get the user name from identity else it was coming as empty.
The above code is sufficient to make the entire authentication process working but if you have any challenge please reach out I would be happy to help.
Hi,
I tried your approach. I am getting the below error.
Exception: System.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException
Message: IDX10500: Signature validation failed. Unable to resolve SecurityKeyIdentifier: ‘SecurityKeyIdentifier
Regards,
Ganesh Kumar
I am not sure about this one and it’s been a while I looked at the code, see here it might help – http://stackoverflow.com/questions/24657307/securitytokensignaturekeynotfoundexception-when-validating-jwt-signature
Hi,
I can able to fix the issue by validating to correct certificate. However I am trying to get the Preferred_Username or Name identifier to create Virtual user. However I am getting some GUID for Name Identifier and hashed value for Preferred_username. Let me know how I can get the correct value.
Glad to hear that. How you are doing your authentication? Is it via OpenId Connect then what is the source of user store? You don’t need to create the virtual user, if you are following both the article then it should give the SitecoreIdentity (and ClaimsIdentity) which is already authenticated.
I used the code as mentioned in this article. I tried to log the Context User details to check the value. But I am getting the below logs.
Context User: False
IsLoggedIn: False
name: extranet\Anonymous
Can you point me where you are assigning the Sitecoreidentity.
Actually, it’s a combination of AuthenticationSwitchingProvider, ClaimsAuthenticationProvider and ClaimsAuthenticationHelper which makes this works. Let me know if you want, we can do sharing session and get you going.
Thanks,
CPrakash, your solution looks promising, but you have things in parts, finding difficult to put things together and make it work with sitecore and identityserver3. Do you have any complete samples in GIT or any other source? I think this may be helpful to many people like me, look forward for your earliest reply.
Thanks Siva for your feedback. I understand the difficulty in integrating everything together. From Sitecore perspective if you use the code from GitHub repository it should work without any change. IdentityServer has more documentation than I can describe from basic setup to experience user.
Let me know if you need any help in setting up the sample, I can help.
Have you got this to work with Resource Owner flow?
I am using Implicit flow where I get id_token containing user claims but in case of ‘resource owner password’ flow you will get access_token which requires you to present this token to IdentityServer and retrieve the user information by calling the user info endpoint.
A quick look gives me this sample code here – https://github.com/IdentityServer/IdentityServer3/issues/2183