Sitecore extranet authentication with OpenIdConnect

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,

  1. Authentication Server
  2. 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

  1. SignIn method- SignIn method is responsible to redirect the user to Authentication server to provider the user id and password and validate the credential.
  2. 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.

About cprakash

A developer, passionate about .Net, Architecture and Security.
This entry was posted in Framework, Security, Sitecore, Sitecore MVC and tagged , , , . Bookmark the permalink.

10 Responses to Sitecore extranet authentication with OpenIdConnect

  1. Ganesh Kumar Subramanian says:

    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

  2. Ganesh Kumar Subramanian says:

    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.

    • cprakash says:

      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,

  3. 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.

    • cprakash says:

      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.

  4. Dwight says:

    Have you got this to work with Resource Owner flow?

Leave a comment