Created by

ITfoxtec
Anders Revsgaard

Support

If you have questions please ask them on Stack Overflow. Tag your questions with 'itfoxtec-identity-saml2' and I will answer as soon as possible.

Package

Releases
NuGet ITfoxtec Identity SAML 2.0
NuGet ITfoxtec Identity SAML 2.0 MVC
NuGet ITfoxtec Identity SAML 2.0 MVC Core

Code and license

GitHub code and example
Open-source license

ITfoxtec Identity SAML 2.0

The open-source ITfoxtec Identity Saml2 package adds SAML-P support for both Identity Provider (IdP) and Relying Party (RP) on top of the SAML 2.0 functionality implemented in .NET.

  • Support .NET 8.0
  • Support .NET 7.0
  • Support .NET 6.0
  • Support .NET 5.0
  • Support .NET Core 3.1
  • Support .NET Standard 2.1
  • Support .NET Framework 4.6.2 and 4.7.2

The ITfoxtec Identity Saml2 package implements the most important parts of the SAML-P standard and some optional features. Message signing and validation as well as decryption is supported. The package supports SAML 2.0 login, logout, single logout and metadata. Both SP Initiated and IdP Initiated sign on is supported.

SAML 2.0 to OpenID Connect 1.0 bridge

You can sign up for free and use FoxIDs as a bridge from SAML 2.0 to OpenID Connect. FoxIDs handles the SAML 2.0 traffic to the Identity Provider (IdP) and your application connects to FoxIDs with OpenID Connect.

SAML 2.0 is an old standard with its shortcomings, and therefore it is often a better choice to use OpenID Connect in an application.


You can likewise use FoxIDs to translate from the Danish NemLog-in3 (MitID) and Context Handler to OpenID Connect.

The ITfoxtec Identity Saml2 package supports having the signing/encryption certificates in Azure Key Vault. Please see the TestWebAppCoreAzureKeyVault sample.

The ITfoxtec Identity Saml2 package is tested for compliance with AD FS, Azure AD, Azure AD B2C, the Danish NemLog-in3 (MitID), the Danish Context Handler and many other IdPs and RPs.

Please see the test samples. The TestWebAppCoreNemLogin3Sp sample show how to implement an NemLog-in3 Service Provider (SP).

You can use the SAML 2.0 tool to decode tokens and create self-signed certificates with the certificate tool.

Supported bindings:

  • Redirect Binding
  • Post Binding
  • Artifact Binding

The bindings can be used as needed for:

  • Authn Request
  • Authn Response (SAML 2.0 Response)
  • Logout Request
  • Logout Response

SHA1/SHA256/SHA384/SHA512 is supported for message signing.

  • SHA1: http://www.w3.org/2000/09/xmldsig#rsa-sha1
  • SHA256: http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
  • SHA384: http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
  • SHA512: http://www.w3.org/2001/04/xmldsig-more#rsa-sha512

ASP.NET MVC and ASP.NET MVC Core is supported by the ITfoxtec Identity SAML 2.0 MVC and MVC Core packages which helps to integrate the ITfoxtec SAML 2.0 package into a MVC og MCV Core application.

Code

The code shown is only a selection of the example code in GitHub.

The ITfoxtec Identity Saml2 package is integrated into a ASP.NET MVC Core Relying Party (RP) application by configuration in Startup and adding an Auth Controller with the following four methods. The binding shown can be changed as needed depending on the requirements.

It is furthermore possible to set some optional parameters on the Saml2AuthnRequest and Saml2LogoutRequest. On the Saml2AuthnRequest e.g. ForceAuthn is supported, which will force the user to enter login credentials even though an SSO context already exists on the Security Token Service (STS) / Identity Provider (IdP).


Add configuration to the ConfigureServices method in Startup

Configuration using IdP metadata.
services.Configure<Saml2Configuration>(Configuration.GetSection("Saml2"));
services.Configure<Saml2Configuration>(saml2Configuration =>
{
    saml2Configuration.SigningCertificate = CertificateUtil.Load(AppEnvironment.MapToPhysicalFilePath(
        Configuration["Saml2:SigningCertificateFile"]), Configuration["Saml2:SigningCertificatePassword"]);
    saml2Configuration.AllowedAudienceUris.Add(saml2Configuration.Issuer);

    var entityDescriptor = new EntityDescriptor();
    entityDescriptor.ReadIdPSsoDescriptorFromUrl(new Uri(Configuration["Saml2:IdPMetadata"]));
    if (entityDescriptor.IdPSsoDescriptor != null)
    {
        saml2Configuration.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;
        saml2Configuration.SingleLogoutDestination = entityDescriptor.IdPSsoDescriptor.SingleLogoutServices.First().Location;
        saml2Configuration.SignatureValidationCertificates.AddRange(entityDescriptor.IdPSsoDescriptor.SigningCertificates);
    }
    else
    {
        throw new Exception("IdPSsoDescriptor not loaded from metadata.");
    }
});
services.AddSaml2();   

Configuration without metadata.
services.Configure<Saml2Configuration>(Configuration.GetSection("Saml2"));
services.Configure<Saml2Configuration>(saml2Configuration =>
{
    saml2Configuration.SigningCertificate = CertificateUtil.Load(AppEnvironment.MapToPhysicalFilePath(
        Configuration["Saml2:SigningCertificateFile"]), Configuration["Saml2:SigningCertificatePassword"]);
    saml2Configuration.AllowedAudienceUris.Add(saml2Configuration.Issuer);

    saml2Configuration.SignatureValidationCertificates.Add(CertificateUtil.Load(AppEnvironment.MapToPhysicalFilePath(
        Configuration["Saml2:SignatureValidationCertificateFile"])));
});
services.AddSaml2();   

Login method in the Auth Controller

[Route("Login")]
public IActionResult Login(string returnUrl = null)
{
    var binding = new Saml2RedirectBinding();
    binding.SetRelayStateQuery(new Dictionary<string, string> 
        { { relayStateReturnUrl, returnUrl ?? Url.Content("~/") } });

    return binding.Bind(new Saml2AuthnRequest(config)).ToActionResult();
}

AssertionConsumerService method in the Auth Controller

After successfully or failing login the ACS method receiver the response.
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{       
    var httpRequest = Request.ToGenericHttpRequest(validate: true);
    var saml2AuthnResponse = new Saml2AuthnResponse(config);

    httpRequest.Binding.Unbind(httpRequest, saml2AuthnResponse);
    await saml2AuthnResponse.CreateSession(HttpContext, 
        ClaimsTransform: (claimsPrincipal) => ClaimsTransform.Transform(claimsPrincipal));

    var returnUrl = httpRequest.Binding.GetRelayStateQuery()[relayStateReturnUrl];
    return Redirect(string.IsNullOrWhiteSpace(returnUrl) ? Url.Content("~/") : returnUrl);
}

Logout method in the Auth Controller

[HttpPost("Logout")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
    if (!User.Identity.IsAuthenticated)
    {
        return Redirect(Url.Content("~/"));
    }

    var binding = new Saml2PostBinding();
    var saml2LogoutRequest = await new Saml2LogoutRequest(config, User).DeleteSession(HttpContext);
    return binding.Bind(saml2LogoutRequest).ToActionResult();
}

LoggedOut method in the Auth Controller

After successfully or failing logout the logged out method receive the response.
[Route("LoggedOut")]
public IActionResult LoggedOut()
{
    var httpRequest = Request.ToGenericHttpRequest(validate: true);
    httpRequest.Binding.Unbind(httpRequest, new Saml2LogoutResponse(config));

    return Redirect(Url.Content("~/"));
}

SingleLogout method in the Auth Controller

Receives a Single Logout request and send a response.
[Route("SingleLogout")]
public async Task<IActionResult> SingleLogout()
{
    Saml2StatusCodes status;
    var httpRequest = Request.ToGenericHttpRequest(validate: true);
    var logoutRequest = new Saml2LogoutRequest(config, User);
    try
    {
        httpRequest.Binding.Unbind(httpRequest, logoutRequest);
        status = Saml2StatusCodes.Success;
        await logoutRequest.DeleteSession(HttpContext);
    }
    catch (Exception exc)
    {
        // log exception
        Debug.WriteLine("SingleLogout error: " + exc.ToString());
        status = Saml2StatusCodes.RequestDenied;
    }

    var responseBinding = new Saml2PostBinding();
    responseBinding.RelayState = httpRequest.Binding.RelayState;
    var saml2LogoutResponse = new Saml2LogoutResponse(config)
    {
        InResponseToAsString = logoutRequest.IdAsString,
        Status = status,
    };
    return responseBinding.Bind(saml2LogoutResponse).ToActionResult();
}