/SAML2-ASP.NET

This is an example of how to configure SAML2 in a MVC ASP.NET project. This application delegates the users authentication to OKTA. To do that, I've used the library SAML2 for ASP.NET.

Primary LanguageC#

SAML2-ASP.NET

Recently I've been working in a ASP.NET MVC project that delegates the users authentication to OKTA. To do that, I've used the library SAML2 for ASP.NET.

The SAML2 Library works perfectly. You don't need to do extra code to make it work since it works as a middleware, but you need to configure it properly to work with your application. As there are very few examples in the documentation of how to configure it, I've decided to share the configuration of my ASP.NET MVC 4.6 application configured with SAML2.


Installation

To install the SAML2 library in your project, you may either download the source package and compile it, or install from NuGet package "SAML2". I've installed it from the NuGet package.

Configuration Web.config

The SAML2 library will require several Web.config changes. One new section must be added to the Web.config, and three handlers need to be mapped to use this library.

In the system.web section we need to configure the authentication mode to forms: "The Forms authentication provider is an authentication scheme that makes it possible for the application to collect credentials using an HTML form directly from the client. The client submits credentials directly to your application code for authentication. If your application authenticates the client, it issues a cookie to the client that the client presents on subsequent requests. If a request for a protected resource does not contain the cookie, the application redirects the client to the logon page"

Finally we need to add the Saml20MetadataFetcher Module. This module comes with the SAML2 library. Without it, I had a problem getting the OKTA response correctly because I think this module is the one that is in charge of parse the OKTA response.

<configuration>
  <configSections>
    <section name="saml2" type="SAML2.Config.Saml2Section, SAML2" />
  </configSections>
  
  <system.web>
    <authentication mode="Forms" />
  </system.web>
  
  <system.webServer>
    <handlers>
      <remove name="SAML2.Protocol.Saml20SignonHandler" />
      <remove name="SAML2.Protocol.Saml20LogoutHandler" />
      <remove name="SAML2.Protocol.Saml20MetadataHandler" />
      <add name="SAML2.Protocol.Saml20SignonHandler" verb="*" path="Login.ashx" type="SAML2.Protocol.Saml20SignonHandler, SAML2" />
      <add name="SAML2.Protocol.Saml20LogoutHandler" verb="*" path="Logout.ashx" type="SAML2.Protocol.Saml20LogoutHandler, SAML2" />
      <add name="SAML2.Protocol.Saml20MetadataHandler" verb="*" path="Metadata.ashx" type="SAML2.Protocol.Saml20MetadataHandler, SAML2" />
    </handlers>
    <modules>
      <remove name="Saml20MetadataFetcher" />
      <add name="Saml20MetadataFetcher" type="SAML2.Saml20MetadataFetcherModule" preCondition="managedHandler" />
    </modules>
  </system.webServer>
  
  <saml2>
  ...
  </saml>

Section SAML2

The saml2 configuration section is split into several specific configuration areas. You have to configure at least 3 of them, the rest are optionals. With those 3 I had my SAML2 section working.

allowedAudienceUris +Info

"Configures the SAML AudienceRestrictions values to use when generating assertions."

We need to configure here the ID of the service provider.

serviceProvider +Info

"Configures the service provider information such as endpoints, SAML AuthnContexts, and NameIdFormats."

Again we need to configure here the ID of the service provider.

In my case (OKTA) I was required to get a auto signed certificate, if that's your case, you can use the default certificate of your machine if you are in Windows.

In the endpoints section we need to set up the paths of the ashx files and set the redirectUrl when the action succeded. In this example I set up a redirectUrl to my home controller and this is the one which allows or not the access. You can see more of that controller in the MVC Controller section.

identityProviders +Info

"Configures the identity providers to be used for federation. Can be as simple as defining the location of the IdP metadata, or provide access to overriding IdP metadata with custom values, etc."

Finally we need to specify the configuration information of the Identity Providers. In my case I override the default Provider settings by adding them manually

The parameter "IDPS_DIRECTORY" is the Directory in which we have the metadata of the federation partners. And the parameter "OKTA_ENDPOINT" is the url of okta where you can set the idp.

<saml2>
    <allowedAudienceUris>
      <audience uri="http://localhost:8888/" />
    </allowedAudienceUris>

    <serviceProvider id="http://localhost:8888/" server="http://localhost:8888/">
       <signingCertificate findValue="CN=localhost" storeLocation="LocalMachine" storeName="My" x509FindType="FindBySubjectDistinguishedName" />
      <endpoints>
        <endpoint type="SignOn" localPath="Login.ashx" redirectUrl="~/home/private" />
        <endpoint type="Logout" localPath="Logout.ashx" redirectUrl="~/home/index" />
        <endpoint type="Metadata" localPath="Metadata.ashx" />
      </endpoints>

      <authenticationContexts comparison="Exact">
        <add context="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" referenceType="AuthnContextClassRef" />
      </authenticationContexts>
    </serviceProvider>
    
    <identityProviders metadata="IDPS_DIRECTORY">
      <add id="OKTA_ENDPOINT" default="true">
        <certificateValidations>
          <add type="SAML2.Specification.SelfIssuedCertificateSpecification, SAML2" />
        </certificateValidations>
      </add>
    </identityProviders>
  </saml2>

MVC RouteConfig

Its necessary to add a new route ignore for the ashx files because of MVC gives the responsability of the routing of the request to his routing system and that means the handler endpoints will result in a Not Found Error when we try to access them.

public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
            routes.IgnoreRoute("{resource}.ashx/{*pathInfo}");

            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = "" }
            );
        }
    }

MVC Controllers

The MVC web application has two parts. One public part and another private part. To access to the private part we need to do a login action in OKTA. To separate the actions that a user can or can not do we use the Authorize. With that tag we are sure that before going in one of the sections that have it, it will check if the user have credentials to access it or not.

As you can see in the Login action we do a redirect to the ashx file. This will redirect the action login to OKTA, allowing the user to put his credentials in the OKTA form.

public class HomeController : Controller
    {
        public ActionResult Error()
        {
            return View("~/Views/Error.cshtml");
        }

        public ActionResult Index()
        {
            return View("~/Views/Index.cshtml");
        }
   
        public ActionResult Login()
        {
            return Redirect("~/Login.ashx");
        }

        [Authorize]
        public ActionResult Private()
        {
            return View("~/Views/Private.cshtml");
        }

        [Authorize]
        public ActionResult Logout()
        {
            return Redirect("~/Logout.ashx");
        }
    }

If the user credentials are valid, we can specify the Authorize tag where to redirect the user. In our case, we will redirect him to the private section of the website (home/private). This will go to the controller and the Authorize tag will grant access to the private part.

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/home/private"),
    Provider = new CookieAuthenticationProvider
    {
        // Enables the application to validate the security stamp when the user logs in.
        // This is a security feature which is used when you change a password or add an external login to your account.  
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(30),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});