I am working on a project to integrate our internal SSO with UmbracoCMS so that users can authenticate with SSO using their credentials and then be automatically signed into Umbraco BackOffice. I am using .Net 4.7.2 with Umbraco 8. I installed Package UmbracoCms.IdentityExtensions, Microsoft.Owin.Security.OpenIdConnect, IdentityModel.
Currently, I seem to be able to login correctly to the internal SSO however it redirects me back to the Umbraco login page instead of the Umbraco BackOffice.
After logging in with SSO I see these Cookies.
UmbracoCustomOwinStartup.cs
using Microsoft.Owin;
using Microsoft.Owin.Security.OpenIdConnect;
using Owin;
using Microsoft.Owin.Security;
using Umbraco.Core.Security;
using Umbraco.Web;
using Umbraco.Web.Security;
using UmbracoApp;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security.Notifications;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System;
using Thinktecture.IdentityModel.Client;
using System.Linq;
using Umbraco.Core;
using Umbraco.IdentityExtensions;
using Microsoft.AspNet.Identity;
//To use this startup class, change the appSetting value in the web.config called
// "owin:appStartup" to be "UmbracoCustomOwinStartup"
[assembly: OwinStartup("UmbracoCustomOwinStartup", typeof(UmbracoCustomOwinStartup))]
namespace UmbracoApp
{
public class UmbracoCustomOwinStartup : UmbracoDefaultOwinStartup
{
/// <summary>
/// Configures the <see cref="BackOfficeUserManager"/> for Umbraco
/// </summary>
/// <param name="app"></param>
protected override void ConfigureUmbracoUserManager(IAppBuilder app)
{
// There are several overloads of this method that allow you to customize the BackOfficeUserManager or even custom BackOfficeUserStore.
app.ConfigureUserManagerForUmbracoBackOffice(
Services,
Mapper,
UmbracoSettings.Content,
GlobalSettings,
//The Umbraco membership provider needs to be specified in order to maintain backwards compatibility with the
// user password formats. The membership provider is not used for authentication, if you require custom logic
// to validate the username/password against an external data source you can create create a custom UserManager
// and override CheckPasswordAsync
global::Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider().AsUmbracoMembershipProvider());
}
/// <summary>
/// Configures the back office authentication for Umbraco
/// </summary>
/// <param name="app"></param>
protected override void ConfigureUmbracoAuthentication(IAppBuilder app)
{
app
.UseUmbracoBackOfficeCookieAuthentication(UmbracoContextAccessor, RuntimeState, Services.UserService, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authenticate)
.UseUmbracoBackOfficeExternalCookieAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, PipelineStage.Authenticate)
.UseUmbracoPreviewAuthentication(UmbracoContextAccessor, RuntimeState, GlobalSettings, UmbracoSettings.Security, PipelineStage.Authorize);
var identityOptions = new OpenIdConnectAuthenticationOptions
{
ClientId = <clientIdString>,
SignInAsAuthenticationType = Umbraco.Core.Constants.Security.BackOfficeExternalAuthenticationType,
Authority = "https://sso......",
RedirectUri = "http://localhost:49761/umbraco#/login",
ResponseType = OpenIdConnectResponseType.Code,
Scope = "openid",
PostLogoutRedirectUri = "http://localhost:49761",
ClientSecret = <clientSecretString>,
};
// Configure BackOffice Account Link button and style
identityOptions.ForUmbracoBackOffice("btn-microsoft", "fa-windows");
identityOptions.Caption = "OpenId Connect";
// Fix Authentication Type
identityOptions.AuthenticationType = identityOptions.Authority;
// Configure AutoLinking
identityOptions.SetExternalSignInAutoLinkOptions(
new ExternalSignInAutoLinkOptions(autoLinkExternalAccount: true));
identityOptions.Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
},
SecurityTokenValidated = async n =>
{
//Applies "claims transformation", as seen: https://identityserver.github.io/Documentation/docsv2/overview/mvcGettingStarted.html
//So that only the necessary claims are kept for the user's ticket
var id = n.AuthenticationTicket.Identity;
// we want to keep first name, last name, subject and roles
var email = id.FindFirst(JwtClaimTypes.Email);
var givenName = id.FindFirst(JwtClaimTypes.GivenName);
var familyName = id.FindFirst(JwtClaimTypes.FamilyName);
var sub = id.FindFirst(JwtClaimTypes.Subject);
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
JwtClaimTypes.GivenName,
JwtClaimTypes.Role);
nid.AddClaim(email);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaim(sub);
// keep the id_token for logout
nid.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
nid.AddClaim(new Claim(ClaimTypes.NameIdentifier
, sub.Value, "http://www.w3.org/2001/XMLSchema#string", DefaultAuthenticationTypes.ExternalCookie));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
// SecurityTokenValidated = ClaimsTransformer.GenerateUserIdentityAsync
};
identityOptions.SignInAsAuthenticationType = Umbraco.Core.Constants.Security.BackOfficeExternalAuthenticationType;
app.UseOpenIdConnectAuthentication(identityOptions);
app.UseSignalR(GlobalSettings);
app.FinalizeMiddlewareConfiguration();
}
}
public class ClaimsTransformer
{
public static async Task GenerateUserIdentityAsync(
SecurityTokenValidatedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> notification)
{
var identityUser = new ClaimsIdentity(
notification.AuthenticationTicket.Identity.Claims,
notification.AuthenticationTicket.Identity.AuthenticationType,
ClaimTypes.Name,
ClaimTypes.Role);
var newIdentityUser = new ClaimsIdentity(identityUser.AuthenticationType,
ClaimTypes.GivenName, ClaimTypes.Role);
newIdentityUser.AddClaim(identityUser.FindFirst(ClaimTypes.NameIdentifier));
var emailClaim = identityUser.FindFirst(ClaimTypes.Email) ?? new Claim(ClaimTypes.Email, identityUser.FindFirst("name").Value);
newIdentityUser.AddClaim(emailClaim);
//Optionally add other claims
var userInfoClient = new UserInfoClient(new Uri(notification.Options.Authority + "/protocol/openid-connect/userinfo"), notification.ProtocolMessage.AccessToken);
var userInfo = await userInfoClient.GetAsync();
newIdentityUser.AddClaims(userInfo.Claims.Select(t => new Claim(t.Item1, t.Item2)));
notification.AuthenticationTicket = new AuthenticationTicket(newIdentityUser,
notification.AuthenticationTicket.Properties);
// notification.AuthenticationTicket.Properties.RedirectUri = "http://localhost:49761/umbraco#/content";
}
}
}
Web.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<!--
Define the web.config template, which is used when creating the initial web.config,
and then transforms from web.Template.[Debug|Release].config are applied. Documentation
for web.config at https://our.umbraco.com/documentation/using-umbraco/config-files/#webconfig
-->
<configSections>
<section name="clientDependency" type="ClientDependency.Core.Config.ClientDependencySection, ClientDependency.Core" requirePermission="false" />
<sectionGroup name="umbracoConfiguration">
<section name="settings" type="Umbraco.Core.Configuration.UmbracoSettings.UmbracoSettingsSection, Umbraco.Core" requirePermission="false" />
<section name="HealthChecks" type="Umbraco.Core.Configuration.HealthChecks.HealthChecksSection, Umbraco.Core" requirePermission="false" />
</sectionGroup>
<sectionGroup name="imageProcessor">
<section name="security" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageSecuritySection, ImageProcessor.Web" />
<section name="processing" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageProcessingSection, ImageProcessor.Web" />
<section name="caching" requirePermission="false" type="ImageProcessor.Web.Configuration.ImageCacheSection, ImageProcessor.Web" />
</sectionGroup>
</configSections>
<umbracoConfiguration>
<settings configSource="config\umbracoSettings.config" />
<HealthChecks configSource="config\HealthChecks.config" />
</umbracoConfiguration>
<clientDependency configSource="config\ClientDependency.config" />
<appSettings>
<add key="Umbraco.Core.ConfigurationStatus" value="8.4.0" />
<add key="Umbraco.Core.ReservedUrls" value="" />
<add key="Umbraco.Core.ReservedPaths" value="" />
<add key="Umbraco.Core.Path" value="~/umbraco" />
<add key="Umbraco.Core.HideTopLevelNodeFromPath" value="true" />
<add key="Umbraco.Core.TimeOutInMinutes" value="20" />
<add key="Umbraco.Core.DefaultUILanguage" value="en-US" />
<add key="Umbraco.Core.UseHttps" value="false" />
<add key="ValidationSettings:UnobtrusiveValidationMode" value="None" />
<add key="webpages:Enabled" value="false" />
<add key="enableSimpleMembership" value="false" />
<add key="autoFormsAuthentication" value="false" />
<add key="dataAnnotations:dataTypeAttribute:disableRegEx" value="false" />
<add key="owin:appStartup" value="UmbracoCustomOwinStartup" />
<add key="Umbraco.ModelsBuilder.Enable" value="true" />
<add key="Umbraco.ModelsBuilder.ModelsMode" value="PureLive" />
<add key="ActiveDirectoryDomain" value="myDomainString" />
</appSettings>
<!--
Important: If you're upgrading Umbraco, do not clear the connection
string / provider name during your web.config merge.
-->
<connectionStrings>
<remove name="umbracoDbDSN" />
<add name="umbracoDbDSN" connectionString="Server=SQLEXPRESS;Database=UmbracoCMS;Integrated Security=true" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SqlServerCe.4.0" />
<add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe" />
</DbProviderFactories>
</system.data>
<system.net>
<mailSettings>
<!--
If you need Umbraco to send out system mails (like reset password and invite user),
you must configure your SMTP host here - for example:
<smtp from="noreply@example.com">
<network host="127.0.0.1" userName="username" password="password" />
</smtp>
-->
</mailSettings>
</system.net>
<system.web>
<customErrors mode="RemoteOnly" />
<trace enabled="false" requestLimit="10" pageOutput="false" traceMode="SortByTime" localOnly="true" />
<httpRuntime requestValidationMode="2.0" enableVersionHeader="false" targetFramework="4.7.2" maxRequestLength="51200" fcnMode="Single" />
<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,Umbraco.Web" />
<add name="ClientDependencyModule" type="ClientDependency.Core.Module.ClientDependencyModule, ClientDependency.Core" />
<add name="ImageProcessorModule" type="ImageProcessor.Web.HttpModules.ImageProcessingModule, ImageProcessor.Web" />
</httpModules>
<httpHandlers>
<remove verb="*" path="*.asmx" />
<add verb="*" path="*.asmx" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false" />
<add verb="*" path="*_AppService.axd" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false" />
<add verb="GET,HEAD" path="ScriptResource.axd" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validate="false" />
<add verb="*" path="DependencyHandler.axd" type="ClientDependency.Core.CompositeFiles.CompositeDependencyHandler, ClientDependency.Core " />
</httpHandlers>
<compilation defaultLanguage="c#" debug="true" batch="true" targetFramework="4.7.2" numRecompilesBeforeAppRestart="50" />
<authentication mode="Forms">
<forms name="yourAuthCookie" loginUrl="login.aspx" protection="All" path="/" />
</authentication>
<authorization>
<allow users="?" />
</authorization>
<!-- Membership Provider -->
<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
<providers>
<clear />
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco.Web" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" allowManuallyChangingPassword="false" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco.Web" />
</providers>
</membership>
<!-- Role Provider -->
<roleManager enabled="true" defaultProvider="UmbracoRoleProvider">
<providers>
<clear />
<add name="UmbracoRoleProvider" type="Umbraco.Web.Security.Providers.MembersRoleProvider" />
</providers>
</roleManager>
<machineKey validationKey="43BE1ECE1116BC91F1E8E8D525A2CA31E4D5825DB8145147F510ED9ACA74765E" decryptionKey="677B048B6C986E840AE995C2A4CE1AB4815416322D35F5153AFF7E775C94CB21" validation="HMACSHA256" decryption="AES" /></system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true">
<remove name="WebDAVModule" />
<remove name="UmbracoModule" />
<remove name="ScriptModule" />
<remove name="ClientDependencyModule" />
<remove name="FormsAuthentication" />
<remove name="ImageProcessorModule" />
<add name="UmbracoModule" type="Umbraco.Web.UmbracoModule,Umbraco.Web" />
<add name="ScriptModule" preCondition="managedHandler" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add name="ClientDependencyModule" type="ClientDependency.Core.Module.ClientDependencyModule, ClientDependency.Core" />
<!-- Needed for login/membership to work on homepage (as per http://stackoverflow.com/questions/218057/httpcontext-current-session-is-null-when-routing-requests) -->
<add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
<add name="ImageProcessorModule" type="ImageProcessor.Web.HttpModules.ImageProcessingModule, ImageProcessor.Web" />
</modules>
<handlers accessPolicy="Read, Write, Script, Execute">
<remove name="WebServiceHandlerFactory-Integrated" />
<remove name="ScriptHandlerFactory" />
<remove name="ScriptHandlerFactoryAppServices" />
<remove name="ScriptResource" />
<remove name="ClientDependency" />
<remove name="MiniProfiler" />
<remove name="ExtensionlessUrlHandler-Integrated-4.0" />
<remove name="OPTIONSVerbHandler" />
<remove name="TRACEVerbHandler" />
<add name="ScriptHandlerFactory" verb="*" path="*.asmx" preCondition="integratedMode" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add name="ScriptHandlerFactoryAppServices" verb="*" path="*_AppService.axd" preCondition="integratedMode" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add name="ScriptResource" verb="GET,HEAD" path="ScriptResource.axd" preCondition="integratedMode" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<add verb="*" name="ClientDependency" preCondition="integratedMode" path="DependencyHandler.axd" type="ClientDependency.Core.CompositeFiles.CompositeDependencyHandler, ClientDependency.Core" />
<add name="MiniProfiler" path="mini-profiler-resources/*" verb="*" type="System.Web.Routing.UrlRoutingModule" resourceType="Unspecified" preCondition="integratedMode" />
<add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" />
</handlers>
<staticContent>
<remove fileExtension=".air" />
<mimeMap fileExtension=".air" mimeType="application/vnd.adobe.air-application-installer-package+zip" />
<remove fileExtension=".svg" />
<mimeMap fileExtension=".svg" mimeType="image/svg+xml" />
<remove fileExtension=".woff" />
<mimeMap fileExtension=".woff" mimeType="application/x-font-woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="application/x-font-woff2" />
<remove fileExtension=".less" />
<mimeMap fileExtension=".less" mimeType="text/css" />
<remove fileExtension=".mp4" />
<mimeMap fileExtension=".mp4" mimeType="video/mp4" />
<remove fileExtension=".json" />
<mimeMap fileExtension=".json" mimeType="application/json" />
</staticContent>
<!-- Ensure the powered by header is not returned -->
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
</customHeaders>
</httpProtocol>
<!-- Increase the default upload file size limit -->
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="52428800" />
</requestFiltering>
</security>
<!--
If you wish to use IIS rewrite rules, see the documentation here:
https://our.umbraco.com/documentation/Reference/Routing/IISRewriteRules
-->
<!--
<rewrite>
<rules></rules>
</rewrite>
-->
</system.webServer>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.1.0.0" newVersion="4.1.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.2.3.0" newVersion="1.2.3.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Cors" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<location path="umbraco">
<system.webServer>
<urlCompression doStaticCompression="false" doDynamicCompression="false" dynamicCompressionBeforeCache="false" />
</system.webServer>
</location>
<location path="App_Plugins">
<system.webServer>
<urlCompression doStaticCompression="false" doDynamicCompression="false" dynamicCompressionBeforeCache="false" />
</system.webServer>
</location>
<imageProcessor>
<security configSource="config\imageprocessor\security.config" />
<caching configSource="config\imageprocessor\cache.config" />
<processing configSource="config\imageprocessor\processing.config" />
</imageProcessor>
<system.codedom>
<compilers>
<compiler language="c#;cs;csharp" extension=".cs" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.CSharpCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:7 /nowarn:1659;1699;1701" />
<compiler language="vb;vbs;visualbasic;vbscript" extension=".vb" type="Microsoft.CodeDom.Providers.DotNetCompilerPlatform.VBCodeProvider, Microsoft.CodeDom.Providers.DotNetCompilerPlatform, Version=2.0.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" warningLevel="4" compilerOptions="/langversion:14 /nowarn:41008 /define:_MYTYPE=\"Web\" /optionInfer+" />
</compilers>
</system.codedom>
</configuration>
I think I need to send the user attributes as claims to the Umbraco BackOffice, but SecurityTokenValidated is not being fired. Any idea where to go from here?
Thank you in advance. Any help is very appreciated!
Resource Link: https://yuriburger.net/2017/04/26/login-to-umbraco-backoffice-using-identityserver4/