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.