On Sitecore 9.0.1 and SXA 1.6 I'm trying to get federated authentication to work for website visitors using OWIN WSFed following this guide in a multi-site SXA solution.
I've gotten everything to work using a page with a controller rendering pointing to an action with the [Authorize] attribute, but that's not quite ideal and in stead I want to use sign-in links described in the linked chapter above.
The problem I'm facing is the link is generated correctly (/identity/externallogin?authenticationType=[etc]), but when I click on it I get an IIS 404 page, leading me to believe OWIN is not actually listening here. I've copied the HandleLoginLink class into my own solution so I can debug it so I can verify OWIN is, indeed, listening at this path. But the way it is implemented OWIN only listens for a path and not a complete URL so it should work for all my sites right? Well...
Now this link is generated and presented on mySite, so the complete link when clicked is http://www-local.mysite.sc/identity/externallogin?authenticationType=[etc]. When I however change the link to include the hostname of another site, http://www-local.othersite.sc/identity/externallogin?authenticationType= and it points to the url of another configured site, suddenly I do enter the OWIN middleware! It returns a 401 but that's expected because I haven't configured fedauth on that site and, moreover, a couple of querystring parameters still reference mySite, but at least something is answering.
Now for my question: Why is OWIN only listening on the sites that don't have a MapEntry in the IdentityProvidersPerSite config?
Here's my config:
<federatedAuthentication type="Sitecore.Owin.Authentication.Configuration.FederatedAuthenticationConfiguration, Sitecore.Owin.Authentication">
<identityProvidersPerSites hint="list:AddIdentityProvidersPerSites">
<mapEntry name="ADFS" type="Sitecore.Owin.Authentication.Collections.IdentityProvidersPerSitesMapEntry, Sitecore.Owin.Authentication">
<sites hint="list">
<site>MySite</site>
</sites>
<identityProviders hint="list:AddIdentityProvider">
<identityProvider ref="federatedAuthentication/identityProviders/identityProvider[@id='ADFS']" />
</identityProviders>
<externalUserBuilder type="Sitecore.Owin.Authentication.Services.DefaultExternalUserBuilder, Sitecore.Owin.Authentication">
<param desc="isPersistentUser">false</param>
</externalUserBuilder>
</mapEntry>
</identityProvidersPerSites>
<identityProviders hint="list:AddIdentityProvider">
<identityProvider id="ADFS" 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 DigiD</caption>
<icon>/sitecore/shell/themes/standard/Custom/24x24/msazure.png</icon>
<domain>sitecore</domain>
<transformations hint="list:AddTransformation">
<!--don't need this anymore, all shared transformations will be done automatically-->
<!--<transformation name="set idp claim" ref="federatedAuthentication/sharedTransformations/setIdpClaim" />-->
</transformations>
</identityProvider>
</identityProviders>
<sharedTransformations hint="list:AddSharedClaimsTransformation">
<setIdpClaim name="setIdpClaim" type="Sitecore.Owin.Authentication.Services.SetIdpClaimTransform, Sitecore.Owin.Authentication"/>
</sharedTransformations>
<propertyInitializer type="Sitecore.Owin.Authentication.Services.PropertyInitializer, Sitecore.Owin.Authentication">
<maps hint="list">
</maps>
</propertyInitializer>
</federatedAuthentication>
<settings>
<setting name="FederatedAuthentication.Enabled">
<patch:attribute name="value">true</patch:attribute>
</setting>
</settings>
<services>
<register serviceType="Sitecore.Abstractions.BaseAuthenticationManager, Sitecore.Kernel"
implementationType="Sitecore.Owin.Authentication.Security.AuthenticationManager, Sitecore.Owin.Authentication"
lifetime="Singleton" />
<register serviceType="Sitecore.Abstractions.BaseTicketManager, Sitecore.Kernel"
implementationType="Sitecore.Owin.Authentication.Security.TicketManager, Sitecore.Owin.Authentication"
lifetime="Singleton" />
<register serviceType="Sitecore.Abstractions.BasePreviewManager, Sitecore.Kernel"
implementationType="Sitecore.Owin.Authentication.Publishing.PreviewManager, Sitecore.Owin.Authentication"
lifetime="Singleton" />
</services>
<pipelines>
<owin.identityProviders>
<processor type="Foundation.Authentication.Pipelines.AdfsIdentityProvidersProcessor, Foundation.Authentication" resolve="true">
<metadataAddress>https://acc.sts.company.nl/FederationMetadata/2007-06/federationmetadata.xml</metadataAddress>
<sites hint="raw:AddSite">
<mysite debug="true"> <!--Specify debug to use manual wsfederation metadata for ACC-->
<name>MySite</name>
<hostname>www-local.mysite.sc</hostname>
<wtrealm>urn:acc-sitecore.company.nl</wtrealm>
<wreply>http://www-local.mysite.sc/login</wreply>
<sessionExpiration>00:15:00</sessionExpiration>
</mysite>
</sites>
</processor>
</owin.identityProviders>
EDIT: I figured my code for the IdentityProviderProcessor wouldn't be amiss too:
public class AdfsIdentityProvidersProcessor : IdentityProvidersProcessor
{
public string MetadataAddress { get; set; }
protected override string IdentityProviderName => "ADFS";
private List<SiteConfiguration> Sites = new List<SiteConfiguration>();
public AdfsIdentityProvidersProcessor(FederatedAuthenticationConfiguration federatedAuthenticationConfiguration)
: base(federatedAuthenticationConfiguration)
{
}
protected override void ProcessCore(IdentityProvidersArgs args)
{
ConfigureAuth(args.App);
}
private void ConfigureAuth(IAppBuilder app)
{
foreach (var site in Sites)
{
app.MapWhen(ctx => ctx.Request.Host.Value.Equals(site.Hostname), siteApp =>
{
siteApp.UseWsFederationAuthentication(CreateWsFederationAuthenticationOptions(site));
});
}
}
private WsFederationAuthenticationOptions CreateWsFederationAuthenticationOptions(SiteConfiguration siteConfig)
{
var options = new WsFederationAuthenticationOptions
{
AuthenticationType = WsFederationAuthenticationDefaults.AuthenticationType,
Wtrealm = siteConfig.Wtrealm,
Wreply = siteConfig.Wreply,
MetadataAddress = MetadataAddress,
UseTokenLifetime = true,
SignInAsAuthenticationType = GetAuthenticationType(),
Notifications = new WsFederationAuthenticationNotifications
{
SecurityTokenValidated = (context) =>
{
context.AuthenticationTicket.Identity.ApplyClaimsTransformations(new TransformationContext(FederatedAuthenticationConfiguration, GetIdentityProvider()));
return Task.CompletedTask;
}
}
};
if (siteConfig.Debug)
{
options.Configuration = new Microsoft.IdentityModel.Protocols.WsFederationConfiguration
{
Issuer = "http://acc.sts.company.nl/adfs/services/trust",
TokenEndpoint = "https://acc.sts.company.nl/adfs/ls/"
};
var cert = new X509Certificate2(Convert.FromBase64String("[certificate]"));
var key = new X509SecurityKey(cert);
options.Configuration.SigningKeys.Add(key);
}
return options;
}
public void AddSite(string key, XmlNode node)
{
AddSite(node);
}
public void AddSite(XmlNode node)
{
Assert.IsTrue(XmlUtil.HasChildNode("wtrealm", node), $"ADFS site config has no wtrealm node: {node.OuterXml}");
Assert.IsTrue(XmlUtil.HasChildNode("hostname", node), $"ADFS site config has no hostname node: {node.OuterXml}");
var expiration = new TimeSpan(0, 15, 0);
if (XmlUtil.HasChildNode("sessionExpiration", node))
{
if (TimeSpan.TryParse(XmlUtil.GetChildValue("sessionExpiration", node), out var newExpiration))
{
expiration = newExpiration;
}
else
{
Log.Warn("sessionExpiration failed to parse. Please use hh:mm:ss format", this);
}
}
Sites.Add(new SiteConfiguration
{
Name = XmlUtil.GetChildValue("name", node),
Hostname = XmlUtil.GetChildValue("hostname", node),
Wtrealm = XmlUtil.GetChildValue("wtrealm", node),
Wreply = XmlUtil.GetChildValue("wreply", node),
Debug = XmlUtil.GetAttribute("debug", node, "false") == "true",
ExpirationTime = expiration
});
}
}
And the part that generates the login link:
public class AdfsAuthenticationController : StandardController
{
public ActionResult AuthenticationTest()
{
return View("~/Views/Authentication/AuthenticationTest.cshtml");
}
public override ActionResult Index()
{
var url = "/";
if (!string.IsNullOrEmpty(Request.QueryString?["ReturnUrl"]))
{
url = Request.QueryString["ReturnUrl"];
}
var corePipelineManager = DependencyResolver.Current.GetService<BaseCorePipelineManager>();
var args = new GetSignInUrlInfoArgs(Context.Site.Name, url);
GetSignInUrlInfoPipeline.Run(corePipelineManager, args);
return View("~/Views/Authentication/LoginLinks.cshtml", args.Result);
}
}
And its view (yeah I verbatim copied some stuff, but hey we never got ahead by re-inventing the wheel over and over right)
@using Sitecore
@using Sitecore.Mvc
@model IEnumerable<Sitecore.Data.SignInUrlInfo>
<div id="loginButtons">
@foreach (var signIn in Model)
{
using (Html.BeginForm(null, null, FormMethod.Post, new { @action = signIn.Href }))
{
<button type="submit">
@signIn.Caption
</button>
}
}
</div>