10

I have an ASP.NET MVC 5 application, and need to support SAML 2.0 authentication. I am evaluating Sustainsys.Saml.Mvc. The User.Identity.Name property in my controller is an empty string, while the User.Identity.IsAuthenticated property is true, and can't for the life of me figure it out.

While installing the Sustainsys.Saml2.Mvc NuGet package, I had to do the following things:

  1. Upgrade the .NET framework from 4.5.1 to 4.6.1
  2. Install Sustainsys.Saml2.Mvc v2.2.0
  3. Upgrade the Microsoft.AspNet.Mvc package from 5.2.3 to 5.2.7
  4. Tweaked Web.config settings according to the Sustainsys.Saml2 documentation
  5. Downloaded the .cert and .pfx files from their demo MVC application

Contents of Web.config:

<?xml version="1.0" encoding="utf-8"?>
<!--
  For more information on how to configure your ASP.NET application, please visit
  http://go.microsoft.com/fwlink/?LinkId=301880
  -->
<configuration>
  <configSections>
    <sectionGroup name="elmah">
      <section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah" />
      <section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah" />
      <section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah" />
      <section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah" />
    </sectionGroup>
    <section name="system.identityModel" type="System.IdentityModel.Configuration.SystemIdentityModelSection, System.IdentityModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="system.identityModel.services" type="System.IdentityModel.Services.Configuration.SystemIdentityModelServicesSection, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089" />
    <section name="sustainsys.saml2" type="Sustainsys.Saml2.Configuration.SustainsysSaml2Section, Sustainsys.Saml2" />
  </configSections>
  <connectionStrings>
    ...
  </connectionStrings>
  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
  </appSettings>
  <!--
    For a description of web.config changes see http://go.microsoft.com/fwlink/?LinkId=235367.

    The following attributes can be set on the <httpRuntime> tag.
      <system.Web>
        <httpRuntime targetFramework="4.6.1" />
      </system.Web>
  -->
  <system.web>
    <customErrors mode="Off" />
    <compilation debug="true" targetFramework="4.6.1" />
    <httpRuntime targetFramework="4.6.1" requestValidationMode="2.0" />
    <authentication mode="Forms">
      <forms loginUrl="~/Saml2/SignIn" />
    </authentication>
    <membership defaultProvider="ABC">
      <providers>
        <clear />
        <add name="ABC" type="System.Web.Security.ActiveDirectoryMembershipProvider" connectionStringName="ADConnectionString" attributeMapUsername="sAMAccountName" />
      </providers>
    </membership>
    <httpModules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" />
    </httpModules>
  </system.web>
  <system.webServer>
    <handlers>
      <add name="UrlRoutingHandler" type="System.Web.Routing.UrlRoutingHandler, System.Web, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" path="Authorization/Permissions/*" verb="GET,POST" />
    </handlers>
    <staticContent>
      <remove fileExtension="eot" />
      <remove fileExtension="otf" />
      <remove fileExtension="woff" />
      <remove fileExtension="woff2" />
      <remove fileExtension="ttf" />
      <remove fileExtension="json" />
      <mimeMap fileExtension="eot" mimeType="application/vnd.ms-fontobject" />
      <mimeMap fileExtension="otf" mimeType="application/x-font-opentype" />
      <mimeMap fileExtension="woff" mimeType="application/x-font-woff" />
      <mimeMap fileExtension="woff2" mimeType="application/font-woff2" />
      <mimeMap fileExtension="ttf" mimeType="application/x-font-ttf" />
      <mimeMap fileExtension="json" mimeType="application/json" />
    </staticContent>
    <validation validateIntegratedModeConfiguration="false" />
    <modules>
      <add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorMail" type="Elmah.ErrorMailModule, Elmah" preCondition="managedHandler" />
      <add name="ErrorFilter" type="Elmah.ErrorFilterModule, Elmah" preCondition="managedHandler" />
      <add name="SessionAuthenticationModule" type="System.IdentityModel.Services.SessionAuthenticationModule, System.IdentityModel.Services, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
    </modules>
    <httpErrors errorMode="Detailed" />
  </system.webServer>
  <sustainsys.saml2 entityId="http://localhost/MyMvcApp/Saml2" returnUrl="http://localhost/MyMvcApp/">
    <identityProviders>
      <add entityId="https://stubidp.sustainsys.com/Metadata" signOnUrl="https://stubidp.sustainsys.com/" allowUnsolicitedAuthnResponse="true" binding="HttpRedirect">
        <signingCertificate fileName="~/App_Data/stubidp.sustainsys.com.cer" />
      </add>
    </identityProviders>
    <!--<federations>
      <add metadataLocation="http://localhost:52071/Federation" allowUnsolicitedAuthnResponse="true"/>
    </federations>-->
    <serviceCertificates>
      <add fileName="~/App_Data/Sustainsys.Saml2.Tests.pfx" />
    </serviceCertificates>
  </sustainsys.saml2>
  <system.identityModel.services>
    <federationConfiguration>
      <cookieHandler requireSsl="false" />
    </federationConfiguration>
  </system.identityModel.services>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-3.0.1.0" newVersion="3.0.1.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Newtonsoft.Json" culture="neutral" publicKeyToken="30ad4fe6b2a6aeed" />
        <bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Optimization" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-1.1.0.0" newVersion="1.1.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="WebGrease" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-1.5.2.14234" newVersion="1.5.2.14234" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Autofac" publicKeyToken="17863af14b0044da" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-3.5.0.0" newVersion="3.5.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Oracle.DataAccess" publicKeyToken="89b483f429c47342" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-4.121.2.0" newVersion="4.121.2.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="1.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
        <bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
  <system.net>
    <mailSettings>
      <smtp deliveryMethod="SpecifiedPickupDirectory" from="foo@mycompany.com">
        <network host="smtp.mycompany.com" port="25" defaultCredentials="true" />
        <specifiedPickupDirectory pickupDirectoryLocation="..." />
      </smtp>
    </mailSettings>
  </system.net>
  <elmah>
    <!--
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for
        more information on remote access and securing ELMAH.
    -->
    <security allowRemoteAccess="true" />
    <errorLog type="Elmah.MemoryErrorLog, Elmah" size="50" />
  </elmah>
  <location path="elmah.axd" inheritInChildApplications="false">
    <system.web>
      <httpHandlers>
        <add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" />
      </httpHandlers>
      <!--
        See http://code.google.com/p/elmah/wiki/SecuringErrorLogPages for
        more information on using ASP.NET authorization securing ELMAH.-->
      <authorization>
        <allow users="..." />
        <deny users="*" />
      </authorization>
    </system.web>
    <system.webServer>
      <handlers>
        <add name="ELMAH" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah" preCondition="integratedMode" />
      </handlers>
    </system.webServer>
  </location>
</configuration>

I'm currently hosting my application from my laptop:

  • Windows 10 Enterprise
  • .NET framework up to 4.6.1 is installed
  • IIS 10.0.15063.0
  • Application pool settings in IIS:
    • .NET 4.0
    • Allow 32-bit applications

It is using the mock Identity Provider at: https://stubidp.sustainsys.com/

The XML posted to /MyApp/Saml2/Acs:

<saml2p:Response Destination="http://localhost/MyApp/Saml2/Acs" ID="idb4440bf88fba449f8526760d4330dd53" Version="2.0" IssueInstant="2018-12-20T18:21:22Z" InResponseTo="idd9b8948ac5ac4c389bf65072169464ac"
    xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
    <saml2:Issuer
        xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">https://stubidp.sustainsys.com/Metadata
    </saml2:Issuer>
    <Signature
        xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
            <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
            <Reference URI="#idb4440bf88fba449f8526760d4330dd53">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
                <DigestValue>...</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>...</SignatureValue>
        <KeyInfo>
            <X509Data>
                <X509Certificate>...</X509Certificate>
            </X509Data>
        </KeyInfo>
    </Signature>
    <saml2p:Status>
        <saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
    </saml2p:Status>
    <saml2:Assertion
        xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" ID="_d6ffcd18-44ec-45db-bd74-9cf48ea1cfa2" IssueInstant="2018-12-20T18:21:22Z">
        <saml2:Issuer>https://stubidp.sustainsys.com/Metadata</saml2:Issuer>
        <saml2:Subject>
            <saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">JohnDoe</saml2:NameID>
            <saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml2:SubjectConfirmationData NotOnOrAfter="2018-12-20T18:23:22Z" InResponseTo="..." Recipient="http://localhost/MyApp/Saml2/Acs" />
            </saml2:SubjectConfirmation>
        </saml2:Subject>
        <saml2:Conditions NotOnOrAfter="2018-12-20T18:23:22Z">
            <saml2:AudienceRestriction>
                <saml2:Audience>http://localhost/MyApp/Saml2</saml2:Audience>
            </saml2:AudienceRestriction>
        </saml2:Conditions>
        <saml2:AuthnStatement AuthnInstant="2018-12-20T18:21:22Z" SessionIndex="42">
            <saml2:AuthnContext>
                <saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
            </saml2:AuthnContext>
        </saml2:AuthnStatement>
    </saml2:Assertion>
</saml2p:Response>

I also set the Options.Logger property on the controller, and this is what I'm getting in the debug output:

[Saml2, DEBUG]: Http POST binding extracted message
<saml2p:Response Destination="http://localhost/MyApp/Saml2/Acs" ...>...</saml2p:Response>
[Saml2, DEBUG]: Signature validation passed for Saml Response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id
[Saml2, DEBUG]: Extracted SAML assertion _a5ee9c7f-4ca5-4693-a7f7-301ae5e6d4a6
[Saml2, INFO]: Successfully processed SAML response Microsoft.IdentityModel.Tokens.Saml2.Saml2Id and authenticated JohnDoe

While debugging the application, the User.Identity.Name property in my MVC controller is null. Further inspection of that object:

Screenshot of debug console in Visual Studio

Why is System.Web.Mvc.Controller.User.Identity.Name null after successfully asserting the SAML response?

Greg Burghardt
  • 17,900
  • 9
  • 49
  • 92

3 Answers3

5

You have properly completed the authentication. And you have a working session authentication cookie.

What you don't have however is a claim that matches the default name claim type. You have the NameIdentifier claim (which is what the stub idp supplies by default). But you don't have a claim with the default NameClaimType

The Name property of a ClaimsIdentity is implemented as

return Claims.FirstOrDefault(c => c.Type == NameClaimType)?.Value;

So to get a value on the Name property you either need to change the NameClaimType (can be done by modifying the created identity in the AcsCommandResultCreated notification) or get your Idp to provide an attribute of type http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name. You can do that by adding an attribute at the bottom of the stubidp form.

Anders Abel
  • 67,989
  • 17
  • 150
  • 217
  • That's interesting. I'm going to poke around in the Sustainsys code a little more to see if I just need to pass a customer key=value pair through the fake identity provider, or if there is a defect in the NuGet package. I figured their fake identity provider would pretty much work out-of-the box, but I guess not. – Greg Burghardt Dec 24 '18 at 01:40
  • 1
    *My* fske identity provider works out of the box with *my* library. But by default, it only provides the bare minimum that is required by the SAML2 standard. The `ClaimsIdentity.Name` property expects another attribute. I actually considered this a feature when implementing the stubidp: to see how asp.net behaves with only the minimum information required by SAML2. – Anders Abel Dec 24 '18 at 20:19
  • Apparently ASP.NET leaves a little to be desired. I am looking to integrate with a SAML2 identity provider that isn't written in the .NET stack. I wonder if this will be an issue integrating with the real solution too. I added an attribute named `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name` with the value of my username, and things are working fine now. Thanks! – Greg Burghardt Jan 03 '19 at 13:20
2

I'm not using Identity server, but same basic issue:

Look at your Claims under User.Identity.Claims and find one that does have your username.

enter image description here

Then wherever you configure the tokenvalidation you have to put the NameClaimType to be the one you saw above:

Then when the middleware validates it it will copy that claim into UserName. I'm assuming this is some default for Windows authentication [sic].

   var tokenValidationParameters = new TokenValidationParameters
   {
            NameClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
Simon_Weaver
  • 140,023
  • 84
  • 646
  • 689
1

Got the same problem with Core 3.1: The User.Identity.Name property is null, while the User.Identity.IsAuthenticated is true. But you can get the name by using the HttpContectAccessor in your controller:

private readonly IHttpContextAccessor _httpContextAccessor;

public MyController(IHttpContextAccessor httpContextAccessor)
{
   _httpContextAccessor = httpContextAccessor;
}

public void AnyMethod()
{
  var userId = _httpContextAccessor.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier).Value;
  var user = _context.AspNetUsers.Find(userId);
  var name = user.UserName;
  ...
}

In Startup.cs add the service:

services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
Gerard
  • 2,649
  • 1
  • 28
  • 46