Dockerify your Sitecore 9.3 XP development environment - SSL, CM and Identity

Dockerify your Sitecore 9.3 XP development environment - SSL, CM and Identity

Dockerify your Sitecore 9.3 XP development environment - SSL, CM and Identity

Dockerify: To move a program that was run on a monolithic application into a container system like Docker.

You hear Docker much more often in the Sitecore world nowadays mainly to simplify your development environment and reduce the complexity of installs. This was apparent in our recent project which involved Sitecore Commerce and the ease of setting up different devs with different pc specs on Docker for Sitecore.

I want to thank all the community and sitecore contributors for working on the Sitecore Docker repo - https://github.com/Sitecore/docker-images.

I am not an expert in using Docker. Initially I was extremely frustrated with Docker but what I can tell you is that, once you start using it in a real world example, it gets easier as you know more. The more you work with it the better you understand it.

Most of my frustration came from Docker for Windows, which I do not think is as stable as it could be and the public Sitecore for Docker repository. I also feel like people do not share their knowledge as much as they should. What is the point in guarding your knowledge?

I apologize for the long blog post. I have so much to share and I will blog a bit more about what helped me with Docker in future posts.

I suggest the following resources to get into Docker for Sitecore:

SSL

As we were working with Sitecore Commerce Docker containers, we realized that none of the roles were setup for SSL let alone for the Identity server, even though the Identity container was included in the build.

Michael West’s Secure Docker Websites for Sitecore blog post https://michaellwest.blogspot.com/2020/01/secure-docker-websites-for-sitecore.html really helped. I still had to figure out the mechanics and here it is as I understand it.

  • Download Michael’s repo -

     

  • Modify the

    startup/createcert.ps1 
    file and set your wildcard domain, in my case lets say I have
    *.bemyfriend.local

  • Run the

    startup/createcert.ps1 
    in an elevated Terminal of your choice, this will generate the certs, pfx, txt files in the same folder.

  • The script also installs the wildcard cert on your host machine. This prevents issues while rendering the sites or calling api with https.

  • Next modify your docker-compose file, in my case it was docker-compose.xc.sxa.yml. Look at the screenshots below:

CM Docker Compose
Identity Docker Compose

  • The entry point is the script which is run on the container when it starts, by default you would see

    entrypoint: powershell.exe -Command "& C:\\tools\\entrypoints\\sitecore-xc-engine\\Development.ps1 -WatchDirectoryParameters @{ Path = 'C:\\src'; Destination = 'C:\\inetpub\\wwwroot'; ExcludeFiles = @('Web.config'); }" 
    but in our case we will use
    entrypoint: powershell.exe -NoLogo -NoProfile -File C:\\startup\\startup.ps1

  • We need to map our local folder .\startup which has the certs to the container’s C:\startup

  • We also bind port 443 to containers 44002 - the container port can be any unique port value if you choose

  • Set the network alias -

    cm.bemyfriend.local

  • Set the environment variable HOST_HEADER to cm.bemyfriend.local, this is the value which is picked up by the startup script in the entry point

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [string]$EntryPointScriptPath = "C:\tools\entrypoints\iis\Development.ps1"
)
Write-Host "Running startup.ps1"
Import-Module WebAdministration
$website = "Default Web Site"
Write-Host "Checking if $($website) has any existing HTTPS bindings"
$hostHeaders = "${env:HOST_HEADER}".Split(";", [System.StringSplitOptions]::RemoveEmptyEntries)
function Set-HttpBinding {
    param(
        [string]$SiteName,
        [string]$HostHeader
    )
    if ($null -eq (Get-WebBinding -Name $siteName | Where-Object { $_.BindingInformation -eq "*:80:$($hostHeader)" })) {
        Write-Host "Adding a new HTTP binding for $($siteName)"
        $binding = New-WebBinding -Name $siteName -Protocol http -IPAddress * -Port 80 -HostHeader $hostHeader
    } else {
        Write-Host "HTTP binding for $($siteName) already exists"
    }
    if ($null -eq (Get-WebBinding -Name $siteName | Where-Object { $_.BindingInformation -eq "*:443:$($hostHeader)" })) {
        Write-Host "Adding a new HTTPS binding for $($siteName)"
        $securePassword = (Get-Content -Path C:\startup\cert.password.txt) | ConvertTo-SecureString -AsPlainText -Force
        $cert = Import-PfxCertificate -Password $securePassword -CertStoreLocation Cert:\LocalMachine\root -FilePath C:\startup\cert.pfx   
        $thumbprint = $cert.Thumbprint
        $binding = New-WebBinding -Name $siteName -Protocol https -IPAddress * -Port 443 -HostHeader $hostHeader
        $binding = Get-WebBinding -Name $siteName -Protocol https
        $binding.AddSslCertificate($thumbprint, "root")
    } else {
        Write-Host "HTTPS binding for $($siteName) already exists"
    }
}
foreach($hostheader in $hostHeaders) {
    Set-HttpBinding -SiteName $website -HostHeader $hostheader
}

Write-Host "Running $($EntryPointScriptPath)"
& $EntryPointScriptPath
The script looked for the 
${env:HOST_HEADER}
 value and binds the local iis instance. Since our cert is a wildcard *.bemyfriend.local, it lets us set host names for all our containers easily.

Identity Server

Now that we have the SSL certs out of the way, we can tackle the Identity server. As mentioned, the Identity Image is part of the repository but unfortunately the Identity configs are disabled by default.

Word of caution: I ran into some issues while running the Identity Server as 

${REGISTRY}sitecore-xc-identity:${SITECORE_VERSION}-windowsservercore-${WINDOWSSERVERCORE_VERSION}
 (where version=1909) - I was getting No signing credential is configured. Can’t create JWT token error. Since we were not pushing any code to this container it did not make sense. I had to use 
${REGISTRY}sitecore-xc-identity:${SITECORE_VERSION}-windowsservercore-${LEGACY_WINDOWSSERVERCORE_VERSION:-ltsc2019}
 for it to work without any issues (effectively setting it to 1809). Will update later if we manage to find the cause of this error.

If we take a look at the Dockerfile in the CM as shown below, the default scripts enable the following Config which in turn disables the Identity Server. 

Copy-Item -Path 'C:\\inetpub\\wwwroot\\App_Config\\Include\\Examples\\Sitecore.Owin.Authentication.IdentityServer.Disabler.config.example' -Destination 'C:\\inetpub\\wwwroot\\App_Config\\Include\\Sitecore.Owin.Authentication.IdentityServer.Disabler.config'

It also adds an 

IdentityServer.config
 which contain the overridden values for 
identityServerAuthority
 and 
FederatedAuthentication.IdentityServer.RequireHttpsMetadata
.

The correct way is for us to fix the image and rebuild it. The easiest way for now is to reapply the patch configs and deploy those.

Place the 

IdentityServer.config
 in the 
App_Config\Include
 folder:

<?xml version="1.0" encoding="utf-8"?><configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
    </sitecore></configuration>
Place the 
Sitecore.Owin.Authentication.IdentityServer.config
 in the 
App_Config\Include\Sitecore\Owin.Authentication.IdentityServer
 folder and modify only the 
identityServerAuthority
 setting to your Identity Server url:
<?xml version="1.0" encoding="utf-8"?>

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:role="http://www.sitecore.net/xmlconfig/role/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
  <sitecore role:require="Standalone or ContentDelivery or ContentManagement">
    <sc.variable name="identityServerAuthority" value="https://bemyfriendidentityserver.dev.local" />

    <settings>
      
      <setting name="FederatedAuthentication.IdentityServer.Authority" value="$(identityServerAuthority)" />
      
      <setting name="FederatedAuthentication.IdentityServer.ClientId" value="Sitecore" />

      
      

      
      <setting name="FederatedAuthentication.IdentityServer.ResourceOwnerClientId" value="SitecorePassword" />

      
      <setting name="FederatedAuthentication.IdentityServer.RequireHttpsMetadata" value="true" />
    </settings>

    <services>
      <configurator type="Sitecore.Owin.Authentication.IdentityServer.ServicesConfigurator, Sitecore.Owin.Authentication.IdentityServer" />
    </services>

    <pipelines>
      <owin.identityProviders>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.IdentityProviders.ConfigureIdentityServer, Sitecore.Owin.Authentication.IdentityServer" resolve="true" id="SitecoreIdentityServer">
          <scopes hint="list">
            <scope name="openid">openid</scope>
            <scope name="sitecore.profile">sitecore.profile</scope>
          </scopes>
        </processor>
      </owin.identityProviders>
      <owin.initialize>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.InterceptLegacyShellLoginPage, Sitecore.Owin.Authentication.IdentityServer" patch:before="processor[@method='Authenticate']" resolve="true">
          <legacyShellLoginPage>/sitecore/login</legacyShellLoginPage>
        </processor>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.JwtBearerAuthentication, Sitecore.Owin.Authentication.IdentityServer" patch:before="processor[@method='Authenticate']" resolve="true">
          <identityProviderName>SitecoreIdentityServer</identityProviderName>
          <audiences hint="raw:AddAudience">
            <audience value="$(identityServerAuthority)/resources" />
          </audiences>
          <issuers hint="list">
            <issuer>$(identityServerAuthority)</issuer>
          </issuers>
        </processor>
        <processor type="Sitecore.Owin.Authentication.IdentityServer.Pipelines.Initialize.LogoutEndpoint, Sitecore.Owin.Authentication.IdentityServer" resolve="true" patch:before="processor[@method='Authenticate']" />
      </owin.initialize>
    </pipelines>

    <federatedAuthentication>
      <identityProvidersPerSites>
        <mapEntry name="sites with the core and unspecified database">
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='SitecoreIdentityServer']" id="SitecoreIdentityServer" />
          </identityProviders>
        </mapEntry>
        
        <!--
        <mapEntry name="all sites">
          <identityProviders hint="list:AddIdentityProvider">
            <identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='SitecoreIdentityServer/IdS4-AzureAd']" />
          </identityProviders>
        </mapEntry>
        -->
      </identityProvidersPerSites>

      <identityProviders>
        <identityProvider id="SitecoreIdentityServer" type="Sitecore.Owin.Authentication.IdentityServer.IdentityServerProvider, Sitecore.Owin.Authentication.IdentityServer" resolve="true">
          <caption>Go to login</caption>
          <domain>sitecore</domain>
          <enabled>true</enabled>
          <triggerExternalSignOut>true</triggerExternalSignOut>
          <transformations hint="list:AddTransformation">
            <transformation name="apply additional claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.ApplyAdditionalClaims, Sitecore.Owin.Authentication.IdentityServer" resolve="true" />
            <transformation name="name to long name" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="name" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
            <transformation name="role to long role" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="role" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://schemas.microsoft.com/ws/2008/06/identity/claims/role" />
              </targets>
              <keepSource>false</keepSource>
            </transformation>
            <transformation name="set ShadowUser" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="http://schemas.microsoft.com/identity/claims/identityprovider" value="local" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://www.sitecore.net/identity/claims/shadowuser" value="true" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
            <!-- owin.cookieAuthentication.signIn pipeline uses http://www.sitecore.net/identity/claims/cookieExp claim to override authentication cookie expiration.
                 'exp' claim value can be configured on Sitecore Identity server on the client configuration by IdentityTokenLifetimeInSeconds setting.
                 Note: Claim value is Unix time expressed as the number of seconds that have elapsed since 1970-01-01T00:00:00Z -->
            <transformation name="use exp claim for authentication cookie expiration" type="Sitecore.Owin.Authentication.Services.DefaultTransformation, Sitecore.Owin.Authentication">
              <sources hint="raw:AddSource">
                <claim name="exp" />
              </sources>
              <targets hint="raw:AddTarget">
                <claim name="http://www.sitecore.net/identity/claims/cookieExp" />
              </targets>
              <keepSource>true</keepSource>
            </transformation>
            <transformation name="remove local role claims" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.RemoveLocalRoles, Sitecore.Owin.Authentication.IdentityServer" />
            <transformation name="adjust NameIdentifier claim" type="Sitecore.Owin.Authentication.IdentityServer.Transformations.AdjustNameIdentifierClaim, Sitecore.Owin.Authentication.IdentityServer" resolve="true" />
          </transformations>
        </identityProvider>
        <!-- An example of how to add an identity provider as a sub-provider of the Identity Server.
             The 'name' property must be in the following format: SitecoreIdentityServer/[AuthenticationScheme], where the 'AuthenticationScheme' equals the
             authentication scheme of an external identity provider that is configured on the Identity Server.

             Notes:
               1. The 'TriggerExternalSignOut' and 'Transformations' properties are inherited from the the Identity Server provider node and can not be overridden.
               2. To use a sub-provider, the 'Enabled' property of the Identity Server provider must be set to 'Enabled'. -->
        <!--
        <identityProvider id="SitecoreIdentityServer/IdS4-AzureAd" type="Sitecore.Owin.Authentication.Configuration.DefaultIdentityProvider, Sitecore.Owin.Authentication">
          <param desc="name">$(id)</param>
          <param desc="domainManager" type="Sitecore.Abstractions.BaseDomainManager" resolve="true" />
          <caption>Log in with Sitecore Identity: Azure AD</caption>
          <icon>/sitecore/shell/themes/standard/Images/24x24/msazure.png</icon>
          <domain>sitecore</domain>
        </identityProvider>
        -->
      </identityProviders>

      <propertyInitializer>
        <maps>
          <map name="set IsAdministrator" type="Sitecore.Owin.Authentication.Services.DefaultClaimToPropertyMapper, Sitecore.Owin.Authentication" resolve="true">
            <data hint="raw:AddData">
              <source name="http://www.sitecore.net/identity/claims/isAdmin" value="true" />
              <target name="IsAdministrator" value="true" />
            </data>
          </map>
        </maps>
      </propertyInitializer>

    </federatedAuthentication>

    <sites>
      <site name="shell" set:loginPage="$(loginPath)shell/SitecoreIdentityServer" />
      <site name="admin" set:loginPage="$(loginPath)admin/SitecoreIdentityServer" />
    </sites>
  </sitecore></configuration>
Once the above is done, file publish your solution to the mapped 
.\data\cm\wwwroot:C:\src
 folder, followed by loading your https://cm.bemyfriend.local in an incognito Chrome browser.

Credit where it's due

If you have any questions, please get in touch with me. https://twitter.com/akshaysura13 on Twitter or on Slack.

KONABOS