Monday, April 25, 2016

How to setup OAuth2.0 integration between IdentityServer3 and Episerver

There exists a couple blog post on how to integrate Episerver with WS-Federation protocol, which also is support in IdentityServer3. But this blog post will introduce to you, how to setup OAuth2.0 integration between Episerver and IdentityServer3. IdentityServer3 is a popular open source security token service framework written in .NET, that implements the OpenID Connect and OAuth2 protocols. In this example we use the “Hybrid”-flow, which also contain the refresh token that’s used to obtain and renewed the identity token. Which is a really nice feature in modern SSO strategy.

For you who want to read an introduction to this, can click here: http://sveinaandahl.blogspot.no/2016/04/how-to-do-identity-and-access.html



Install necessary Nuget Packages
First of all you need to install som nuget packages. Open the Tools > Nuget Package Manager > Package Manager Console and run this commands:

PM> Install-Package Microsoft.Owin.Security.Cookies
PM> Install-Package Microsoft.Owin.Host.SystemWeb
PM> Install-Package Microsoft.Owin.Security.OpenIdConnect


NOTE
Since the implementaion of the OpenID Connect and OAuth2 protocols usually use JSON web token(JWT), the token format look something like this:
//claims
{
 "sub": "svein@knowit.no",
 "name": "Svein Aandahl",
 "role": "Administator"
}

While the URI in a assertion (claims) in a Saml1.1 token looks like this
Role and Name claim
http://schemas.microsoft.com/ws/2008/06/identity/claims/role
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name

Why is this importen to be aware of? The Episerver implementation only support the Saml 1.1 token standard. So when you use the OAuth 2.0 you would need to adjust some of the code to make this work in Episerver.

Code in Startup.cs
Create a startup.cs file on root and add following code.

using EPiServer.Security;
using EPiServer.ServiceLocation;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Cookies;
using Owin;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web.Helpers;
using IdentityModel.Client;
using Microsoft.IdentityModel.Protocols;
using Microsoft.Owin.Security.OpenIdConnect;

[assembly: OwinStartup(typeof(EPiServer.Templates.Alloy.Startup))]

namespace EPiServer.Templates.Alloy
{
    public class Startup
    {
        const string LogoutUrl = "/util/logout.aspx";
        const string LoginUrl = "/login";
        const string clientId = "Client-Name";
        const string IdSrvUrl = "https://idsrv.local";
        const string ClientUrl = "http://website.local/";

        public void Configuration(IAppBuilder app)
        {
            app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

            AntiForgeryConfig.UniqueClaimTypeIdentifier = "sub";
            JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
            JwtSecurityTokenHandler.OutboundClaimTypeMap = new Dictionary<string, string>();

            // Sets the authentication type for the owin middleware
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = "Cookies"
            });

            // Uses OpenIdConnect middleware for authentication
            app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
            {
                ClientId = clientId,

                Authority = IdSrvUrl + "/core/", 
                RedirectUri = ClientUrl, 
                PostLogoutRedirectUri = ClientUrl, 
                ResponseType = "code id_token token",
                Scope = "openid email profile offline_access roles",
                UseTokenLifetime = false, //To avoid that the session expires after 5 min 
                SignInAsAuthenticationType = "Cookies",

                Notifications = new OpenIdConnectAuthenticationNotifications
                {
                    AuthorizationCodeReceived = async n =>
                    {
                        // use the code to get the access and refresh token
                        var tokenClient = new TokenClient(
                            IdSrvUrl + "/core/connect/token",
                            clientId, 
                            "secret");

                        var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(n.Code, n.RedirectUri);

                        // use the access token to retrieve claims from userinfo
                        var userInfoClient = new UserInfoClient(
                            new Uri(IdSrvUrl + "/core/connect/userinfo"),
                            tokenResponse.AccessToken);

                        var userInfoResponse = await userInfoClient.GetAsync();

                        // create new identity
                        var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
                        if (id.IsAuthenticated)
                        { 
                            id.AddClaims(userInfoResponse.GetClaimsIdentity().Claims);
                        
                            // Add name claim as http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
                            var name = id.Claims.Where(x => x.Type == "name").Select(x => x.Value).FirstOrDefault() ?? 
                                       id.Claims.Where(x => x.Type == "preferred_username").Select(x => x.Value).FirstOrDefault();
                            id.AddClaim(new Claim(ClaimTypes.Name, name));

                            // Add all role claims for the user as http://schemas.microsoft.com/ws/2008/06/identity/claims/role
                            IEnumerable<Claim> roles = id.Claims.Where(x => x.Type == "role");
                            foreach (var role in roles)
                            {
                                id.AddClaim(new Claim(ClaimTypes.Role, role.Value));
                            }

                            id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
                            id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
                            id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
                            id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
                        }

                        n.AuthenticationTicket = new AuthenticationTicket(
                            new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType),
                            n.AuthenticationTicket.Properties);

                    },

                    RedirectToIdentityProvider = n =>
                    {
                        // if signing out, add the id_token_hint
                        if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
                        {
                            var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
                            if (idTokenHint != null)
                            {
                                n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
                            }
                        }
                        return Task.FromResult(0);
                    },

                    SecurityTokenValidated = (ctx) =>
                    {
                        //Ignore scheme/host name in redirect Uri to make sure a redirect to HTTPS does not redirect back to HTTP
                        var redirectUri = new Uri(ctx.AuthenticationTicket.Properties.RedirectUri, UriKind.RelativeOrAbsolute);
                        if (redirectUri.IsAbsoluteUri)
                        {
                            ctx.AuthenticationTicket.Properties.RedirectUri = redirectUri.PathAndQuery;
                        }

                        //Sync user and the roles to EPiServer in the background
                        ServiceLocator.Current.GetInstance<OAuth2SynchronizingUserService>().SynchronizeAsync(ctx.AuthenticationTicket.Identity);

                        return Task.FromResult(0);
                    },
                }
            });
            //Remap login
            app.Map(LoginUrl, map =>
            {
                map.Run(ctx =>
                {
                    if (ctx.Authentication.User == null ||
                        !ctx.Authentication.User.Identity.IsAuthenticated)
                    {
                        ctx.Response.StatusCode = 401;
                    }
                    else
                    {
                        ctx.Response.Redirect("/");
                    }
                    return Task.FromResult(0);
                });
            });
            //Remap logout 
            app.Map(LogoutUrl, map =>
            {
                map.Run(ctx =>
                {
                    ctx.Authentication.SignOut();
                    return Task.FromResult(0);
                });
            });

            //Tell antiforgery to use the name claim
            AntiForgeryConfig.UniqueClaimTypeIdentifier = ClaimTypes.Name;
        }
    }
}

To be able to sync roles up from Identity Server when the user login, we need to replace the "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" with "role".

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using EPiServer.Async;
using EPiServer.Framework;
using EPiServer.Security;
using EPiServer.ServiceLocation;

namespace TestSite.Business.ServiceLocation
{
    [ServiceConfiguration(typeof(SynchronizingUserService))]
    public class OAuth2SynchronizingUserService : SynchronizingUserService
    {
        public override Task SynchronizeAsync(ClaimsIdentity identity)
        {
            Validator.ThrowIfNull("identity", identity);
            if (!identity.IsAuthenticated)
                throw new ArgumentException("The identity is not authenticated", nameof(identity));

            var name = identity.Name ?? identity.Claims.Single(c => c.Type == "aud").Value;
            var roles = GetRolesFromClaims(identity);
            return ServiceLocator.Current.GetInstance<TaskExecutor>().Start(() => SynchronizeUserAndRoles(name, roles));
        }

        private static List<string> GetRolesFromClaims(ClaimsIdentity identity)
        {
            return identity.Claims.Where(c => c.Type == "role").Where(c => !string.IsNullOrEmpty(c.Value)).Select(c => 
c.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
        }
    }
}

Web.config
You need to do some modification in web.config and remove membership provider .


    <authentication mode="None"/>

    <membership>
      <providers>
        <clear/>
      </providers>
    </membership>
    <roleManager enabled="false">
      <providers>
        <clear/>
      </providers>
    </roleManager>

<episerver.framework>
    <securityEntity>
      <providers>
        <add name="SynchronizingProvider" type ="EPiServer.Security.SynchronizingRolesSecurityEntityProvider, EPiServer"/>
      </providers>
    </securityEntity>
    <virtualRoles addClaims="true">
       //existing virtual roles
    </virtualRoles>


Administration of users
After you have got everything up and running with both episerver and IdentityServer then you need to go into the admin to create a user and add roles. These roles will be populated in Episerver, so you can add them as groups in the Episerver tree.



To be able to login to Episerver editor you need to add the name and role claim as minimum.




Happy coding!

How to do Identity and Access Management with Episerver

After working with Identity and Access Management (IAM) in a couple of large Episerver projects, I want to share some of the knowledge gained from this work. After Episerver upgraded their CMS platform to use OWIN you can pretty much replace the old membership provider with whatever IAM system you want. In this blog post I will introduce how to use OpenID Connect and OAuth2.0 through Identity Server 3 together with Episerver.

The big advantage of claims based identity is that the burden of authenticating users is moved from the application to an identity server or identity provider. It’s important to understand the difference between authentication and authorization.

The Analogy
Here is an analogy, let's say that you want to get into a nightclub.

First, you need to be authenticated. By issuing a driver's license from the department of Motor Vehicles (analogous to the Identity Server), the state confirms your identity and generates a license, which includes data about you. The data includes information such as your name, your birth-date, your picture and so on. The token that you have been issued proves your identity.

Next, you make your way to the club, with the license in hand. Once you arrive, the bouncer obtains your name from your license and makes an authorization decision. “Is your name on the guest list?” The guest list acts as an approved claim in the system you want to access, which simply lists the people who are authorized to enter. If your name appears, then you are allowed access into the club.

In this rather simplified analogy, there is a clear separation between the entity that performs authentication (the state government), and the entity that performs authorized (the club bouncer).

Different protocols
Today the most common authentication protocols are OpenID Connect, WS-Federation and SAML2P. Where SAML2P are probably the most common protocol. 

Microsoft Corporation has invested heavily into incorporating WS-Federation into its products. The most popular claim type for WS-Federation, ironically, is a SAML 1.1 Assertion. This leads people to think that WS-Federation and SAML can talk to each other. So when people talk about supporting SAML they really talking about support SAML claims inside WS-Federation.

OpenID Connect is only a couple years old, but is considered to be the future, since it provides the most possibilities for modern applications. It was also designed to be more usable by native and mobile applications, and to be WebAPI friendly.

Architecture scenario


Through e.g. a WebAPI you could get more profile data to enrich the information about the logged in user, instead of putting all kind of information into claims.

IdentityServer3
In this scenario, one of the key components is the use of IdentityServer3 to replace the authentication in Episerver or any website. The IdentityServer3 is a popular open source security token service framework written in .NET, that implements the OpenID Connect and OAuth2 protocols. It is used to authenticate users via single-sign-on and to secure WebAPIs. It can be used stand-alone or in conjunction with other identity providers, such as Google, Facebook, Azure AD, ADFS and others.

IdentityServer3 supports different OpenId Connect flows that can be used in to communication between different types of clients.

If you are going to have some form of communication between two systems that doesn’t involve human, e.g. web API’s, then you should look into using "client credentials" flow.

Another one is the "Hybrid" flow. To avoid the users to reauthenticate every time they want to access a restricted area on the website you can use this flow, which contains the refresh token that’s used to obtain and renewed the identity token. I have written another blog post on how to connect Episerver and IdentityServer3 based on the OpenID Connect/OAuth2.0 protocol using the “Hybrid” flow. You can check that here:
http://sveinaandahl.blogspot.no/2016/04/how-to-setup-oauth20-integration.html

In some cases, where you will need to handle login with legacy systems it’s also possible to handle login screen on the website with the Resource Owner Flow, but this is usually not the recommended way. Depends on the goal and what kind of constrains you have.

For User Repository, IdentityServer3 supports both ASP.NET Identity 2.0 and MembershipReboot from Brock Allan. By using MembershipReboot you also have the ability to use nosql databases, like RavenDb and MongoDb.

I have done migration for users from Membership provider, which is the default authentication system in Episerver, to ASP.NET Identity 2.0 and had no problems to validate old passwords afterwards. You can read about that here:
http://sveinaandahl.blogspot.no/2016/03/how-to-validate-old-passwords-when.html

My experience is that ASP.NET Identity 2.0 need some “love” to perform just as good as the MembershipReboot do.  And that ASP.NET Identity 2.0 got less features then MembershipReboot. But both choices are reasonable.

If a company has several brands, each with its own website, they might wish to customize the login screen for each brand. This is possible to do, to some degree with Identity Server. The screen dump below shows the login screen looks like default, customized with Norwegian language.

Extensibility and customization
In some cases, you e.g. want to be able to override the login process to validate other username than email, but also use phone number, member number and so on. Or maybe you want to validate if users exist and then populate claims from the CRM system during login process. Could also happened you want to be able verify that the user exists in the CRM system, before the user are allowed to register.

Since IdentityServer3 is designed for extensibility and customization and allows applications to satisfy their custom security requirements you will be able to do that. But you should always be very careful how your stitching this together to avoid security breach in your system.

Scalability
The IdentityServer3 is very scalable. The application is built in a way that setting it up in a load balanced environment is quite easy. Performance challenges is usually depended on what choices you have made when it comes to user repository and if you have extended and customized the login process in good way.

Links
For those who use Xamarin to build mobile applications on iOS or Android. Here is an example how to integrate Xamarin with Identity Server: 

If you are interested in finding out more about IdentityServer3 it’s a good start to begin here: 

For those who are interested, then Identity Server will also be supported for ASP.NET 5 when it’s launched.

Monday, March 14, 2016

How to validate old passwords when migrating from Membership provider to ASP.NET Identity in Episerver

Wouldn’t be nice to be able to validate the old passwords when you migrate from Membership provider to ASP.NET Identity? This was one of my headaches in an earlier project, but here is the solution.

Prerequisite 
The example in this post are based on the code from here: http://sveinaandahl.blogspot.no/2015/08/how-to-integrate-aspnet-identity-with.html

Note!
One of the things you should check out first in the original Episerver solution's web.config. Here you will find the hash algorithm type that’s used in the Membership provider. In the example below you can see «HMACSHA512» is used. 

From Web.config:

<membership defaultProvider="SqlServerMembershipProvider" userIsOnlineTimeWindow="10" hashAlgorithmType="HMACSHA512">

The whole clue when you migrate the passwords is to put the hashed password together with the password format, that’s always 1, and the password salt. Then you put it into the Hashed Password field in the new database.

e.g.: aspnet_Membership.Password+'|'+ 1 +'|'+aspnet_Membership.PasswordSalt

In the User Manager code you override the PasswordHasher function. When you get a user that has a password with this type of password format use the original way to encrypt the passwords in Membership provider, or else you can use the base version of PasswordHasher. Use the same hash algorithm type that you used to create in the original solution.

IdentityConfig.cs
Override the Password hasher function.

namespace TestSite
{
    // Configure the application user manager used in this application. UserManager is defined in ASP.NET Identity and is used by the application.
    public class ApplicationUserManager : UserManager<ApplicationUser>
    {
        public ApplicationUserManager(ApplicationUserStore store)
            : base(store)
        {
            PasswordHasher = new MyPasswordHasher();
        }
 

MyPasswordHasher.cs
Add a new class called MyPasswordHasher.cs

using System;
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNet.Identity;
 
namespace TestSite
{
    public class MyPasswordHasher : PasswordHasher
    {
        public override string HashPassword(string password)
        {          
            return base.HashPassword(password);
        }
 
        public override PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
        {
            var passwordProperties = hashedPassword.Split('|');
            if (passwordProperties.Length != 3)
            {
                return base.VerifyHashedPassword(hashedPassword, providedPassword);
            }
            else
            {
                var passwordHash = passwordProperties[0];
                const int passwordformat = 1;
                var salt = passwordProperties[2];
                if (String.Equals(EncryptPassword(providedPassword, passwordformat, salt), passwordHash, StringComparison.CurrentCultureIgnoreCase))
                {
                    return PasswordVerificationResult.SuccessRehashNeeded;
                }
                else
                {
                    return PasswordVerificationResult.Failed;
                }
            }
        }
 
        private string EncryptPassword(string pass, int passwordFormat, string salt)
        {
            if (passwordFormat == 0) 
                return pass;
 
            byte[] bIn = Encoding.Unicode.GetBytes(pass);
            byte[] bSalt = Convert.FromBase64String(salt);
            byte[] bRet = null;
 
            if (passwordFormat == 1)
            {  
                HashAlgorithm hm = HashAlgorithm.Create("HMACSHA512"); 
 
                if (hm is KeyedHashAlgorithm)
                {
                    KeyedHashAlgorithm kha = (KeyedHashAlgorithm)hm;
                    if (kha.Key.Length == bSalt.Length)
                    {
                        kha.Key = bSalt;
                    }
                    else if (kha.Key.Length < bSalt.Length)
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        Buffer.BlockCopy(bSalt, 0, bKey, 0, bKey.Length);
                        kha.Key = bKey;
                    }
                    else
                    {
                        byte[] bKey = new byte[kha.Key.Length];
                        for (int iter = 0; iter < bKey.Length; )
                        {
                            int len = Math.Min(bSalt.Length, bKey.Length - iter);
                            Buffer.BlockCopy(bSalt, 0, bKey, iter, len);
                            iter += len;
                        }
                        kha.Key = bKey;
                    }
                    bRet = kha.ComputeHash(bIn);
                }
                else
                {
                    byte[] bAll = new byte[bSalt.Length + bIn.Length];
                    Buffer.BlockCopy(bSalt, 0, bAll, 0, bSalt.Length);
                    Buffer.BlockCopy(bIn, 0, bAll, bSalt.Length, bIn.Length);
                    bRet = hm.ComputeHash(bAll);
                }
            }
            return Convert.ToBase64String(bRet);
        }
 
    }
}

Migration of the database.
Since the database is created by the Entity Framework the first time you run the Episerver application with ASP.NET Identity, I recommend you not to try to use any create tables in the migration script. Since this often end up being a corrupt database for the Entity Framework.

The simple version would be just to do inserts on a existing database. The migration script could look something like the script below. Notice how the password format is set together.

The script below is not well tested, but it gives you an idea how it could work.

INSERT INTO AspNetUsers(Id,Email,EmailConfirmed,PasswordHash,SecurityStamp,
PhoneNumber,PhoneNumberConfirmed,TwoFactorEnabled,LockoutEndDateUtc,LockoutEnabled,
AccessFailedCount,UserName)
SELECT aspnet_Users.UserId,aspnet_Membership.Email,'true',
(aspnet_Membership.Password+'|'+CAST(aspnet_Membership.PasswordFormat as varchar)+'|'+aspnet_Membership.PasswordSalt),
NewID(),NULL,'false','true',aspnet_Membership.LastLockoutDate,'true','0',aspnet_Users.UserName
FROM aspnet_Users
LEFT OUTER JOIN aspnet_Membership ON aspnet_Membership.ApplicationId = aspnet_Users.ApplicationId 
AND aspnet_Users.UserId = aspnet_Membership.UserId;


TIPS
If you are going to run this script several times without getting duplicates you should probably look into using cursor scripts in sql. This way you would avoid inserting a user that's already migrated.

Have fun!

Wednesday, January 13, 2016

Tips for installing Episerver standard search with SSL

I wrote a blog post on how to setup Episerver Standard Search more than three years ago. After getting some questions about how use of SSL together with Episerver standard search, I thought I should share some useful tips here. I noticed that several people have written about this topic earlier, so this is more like an add on to this.


First of all, you should get the solution up and running without SSL. You will find a guide how to do that from this blog:


There are a two elements you will need to add to the web.config, when you want to add ssl support to your solution. Changes are marked with yellow background.

Web.config

 <system.serviceModel>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
    <bindings>
      <webHttpBinding>
        <binding name="IndexingServiceCustomBinding" maxBufferPoolSize="1073741824" maxReceivedMessageSize="2147483647" maxBufferSize="2147483647">
           <security mode="Transport">
              <transport clientCredentialType="None"></transport>
           </security>
          <readerQuotas maxStringContentLength="10000000" />
        </binding>
      </webHttpBinding>
    </bindings>
  </system.serviceModel>



  <episerver.search active="true">
    <namedIndexingServices defaultService="serviceName">
      <services>
        <add name="serviceName" baseUri="https://DNS-name/IndexingService/IndexingService.svc" accessKey="local" />
      </services>
    </namedIndexingServices>
    <searchResultFilter defaultInclude="true">
      <providers />
    </searchResultFilter>
  </episerver.search>


IIS
In IIS you will need to add a https binding and perhaps remove the http. :-)


If everything is working fine you should get up a web service in your browser from this url (including the https): https://YourDnsName/IndexingService/IndexingService.svc

If not, you should read on. :-)

TIPS! «HTTP Activation» is a feature that is often forgot. So it's always smart to check if that's activated for the asp.net version you use in the project.

My experience is that it’s usually two things with the setup that’s not correct, when using SSL together with standard search. One is the lack of knowledge about certificates and how to install them, and second is how to setup the IIS correct.

Different certificates
The most used certificate types are «Domain validated certificates» and «Extended validation certificates». The use of EV certificate can be easily spotted by that the url field in the browser get an «green bar» in the beginning. More and more sites has started to use these EV certificates.They usually cost little more and takes longer to order because of the validation process.

Extended Validation Certificate
EV certificates use the same encryption as domain validated certificates: The increase in security is due to the identity validation process, which is indicated inside the certificated by the policy identifier.

Ref: https://en.wikipedia.org/wiki/Extended_Validation_Certificate


Production environment 
Before IIS8, there was an problem to have more than one certificate per server, since most server have only one IP address. Often this could be solve by using a SAN certificate that contain several domains.

With Windows 2012 (IIS8) came a new feature called Server Name Identification (SNI), which made it possible to use several certificate on same IP.

You can find more information on how to install this things here:

Single Certificate and Multiple Certificates Using SNI (IIS8)
https://www.digicert.com/ssl-certificate-installation-microsoft-iis-8.htm




Development environment
In development we usually just create a self-signed certificate. When you create this in IIS they are usually «Issued To» your computer name. What you write in as Friendly name doesn’t really mater.
So, if you want to avoid to getting a certificate error when you run your site in the browser you should use your machine name as localhost name.

TIPS! If you only are using one certificate you put the certificate into «Personal». If you are going to use SNI, you must add it to «Web Hosting».



How to access the certificate 
If you get access denied in the log file, here is a way to fix that. After you have create the certificate to the Personal folder, then type "MMC" in Windows search and open it up.(got an icon of a red tool box)

Then goto FILE -> Add/Remove Snap-in. -> Select Certificates -> click Add-> Select Computer account -> Local Computer -> Finished -> Ok.

Then open up Certificates -> Personal. Here you will find the certificate you have created or installed earlier.



Add Permissions to the certificate
Right click on the certificate(marked blue on picture above) -> All tasks -> Manage Private Keys -> Add "Network Service" user -> Remove «Full control» (only read is necessary) -> OK


IIS
Now add «Network Service» to the Application Pool to the site you use in IIS for the site with SSL. This will give the App pool identity access to both certificate and the site.



Happy configuring! :-)