Skip to content

Commit

Permalink
add saml2 authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
jxnkwlp committed Aug 18, 2023
1 parent 5cece02 commit 4e816c1
Show file tree
Hide file tree
Showing 13 changed files with 726 additions and 1 deletion.
6 changes: 6 additions & 0 deletions Passingwind.CommonLibs.sln
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ VisualStudioVersion = 17.6.33723.286
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SwaggerExtensions", "src\SwaggerExtensions\SwaggerExtensions\SwaggerExtensions.csproj", "{CC9E858E-8E03-420F-9EA2-90F90485C7CA}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Passingwind.Authentication.Saml2", "src\Passingwind.Authentication.Saml2\Passingwind.Authentication.Saml2.csproj", "{8EF49E09-42AC-4B7C-BDBB-96EDE277F030}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -15,6 +17,10 @@ Global
{CC9E858E-8E03-420F-9EA2-90F90485C7CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC9E858E-8E03-420F-9EA2-90F90485C7CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC9E858E-8E03-420F-9EA2-90F90485C7CA}.Release|Any CPU.Build.0 = Release|Any CPU
{8EF49E09-42AC-4B7C-BDBB-96EDE277F030}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8EF49E09-42AC-4B7C-BDBB-96EDE277F030}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8EF49E09-42AC-4B7C-BDBB-96EDE277F030}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8EF49E09-42AC-4B7C-BDBB-96EDE277F030}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# Common Libs
# Common Libs

## SwaggerExtensions

## Saml2 Authentication
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using ITfoxtec.Identity.Saml2;
using ITfoxtec.Identity.Saml2.Schemas.Metadata;

namespace Passingwind.Authentication.Saml2.Configuration;

public class ConfigurationManager : IConfigurationManager
{
private Saml2Configuration? _saml2Configuration;

private readonly Saml2Options _options;
private readonly Uri _idpMetadataUri;
private readonly HttpClient _httpClient;

public ConfigurationManager(Saml2Options options, Uri idpMetadataUrl, HttpClient httpClient)
{
_options = options;
_idpMetadataUri = idpMetadataUrl;
_httpClient = httpClient;
}

public async Task<Saml2Configuration> GetConfigurationAsync(CancellationToken cancellationToken = default)
{
if (_saml2Configuration != null)
return _saml2Configuration;

var configuration = new Saml2Configuration()
{
Issuer = _options.Issuer,
CertificateValidationMode = _options.CertificateValidationMode,
SigningCertificate = _options.SigningCertificate,
};

_options.SignatureValidationCertificates?.ForEach(configuration.SignatureValidationCertificates.Add);

configuration.AllowedAudienceUris.Add(configuration.Issuer);

var entityDescriptor = new EntityDescriptor();

if (_idpMetadataUri.IsFile)
{
entityDescriptor.ReadIdPSsoDescriptorFromFile(_idpMetadataUri.ToString());
}
else
{
// await entityDescriptor.ReadIdPSsoDescriptorFromUrlAsync(_httpClientFactory, _idpMetadata, cancellationToken);
var metadataGetResponse = await _httpClient.GetAsync(_idpMetadataUri, cancellationToken);
metadataGetResponse.EnsureSuccessStatusCode();

var metadataString = await metadataGetResponse.Content.ReadAsStringAsync();
entityDescriptor.ReadIdPSsoDescriptor(metadataString);
}

if (entityDescriptor.IdPSsoDescriptor != null)
{
configuration.AllowedIssuer = entityDescriptor.EntityId;

configuration.SingleSignOnDestination = entityDescriptor.IdPSsoDescriptor.SingleSignOnServices.First().Location;
configuration.SingleLogoutDestination = entityDescriptor.IdPSsoDescriptor.SingleLogoutServices.First().Location;

foreach (var signingCertificate in entityDescriptor.IdPSsoDescriptor.SigningCertificates)
{
if (signingCertificate.IsValidLocalTime())
{
configuration.SignatureValidationCertificates.Add(signingCertificate);
}
}
if (configuration.SignatureValidationCertificates.Count == 0)
{
throw new Exception("The saml2 idp signing certificates has expired.");
}
if (entityDescriptor.IdPSsoDescriptor.WantAuthnRequestsSigned.HasValue)
{
configuration.SignAuthnRequest = entityDescriptor.IdPSsoDescriptor.WantAuthnRequestsSigned.Value;
}
}
else
{
throw new Exception("The saml2 idp entity descriptor not loaded from metadata.");
}

_saml2Configuration = configuration;
return configuration;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Threading;
using System.Threading.Tasks;
using ITfoxtec.Identity.Saml2;

namespace Passingwind.Authentication.Saml2.Configuration;

public interface IConfigurationManager
{
Task<Saml2Configuration> GetConfigurationAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Threading;
using System.Threading.Tasks;
using ITfoxtec.Identity.Saml2;

namespace Passingwind.Authentication.Saml2.Configuration;

public class StaticConfigurationManager : IConfigurationManager
{
private readonly Saml2Configuration _saml2Configuration;

public StaticConfigurationManager(Saml2Configuration saml2Configuration)
{
_saml2Configuration = saml2Configuration;
}

public Task<Saml2Configuration> GetConfigurationAsync(CancellationToken cancellationToken = default)
{
return Task.FromResult(_saml2Configuration);
}
}
39 changes: 39 additions & 0 deletions src/Passingwind.Authentication.Saml2/Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;

namespace Passingwind.Authentication.Saml2;

static class Extensions
{
public static ITfoxtec.Identity.Saml2.Http.HttpRequest ToGenericHttpRequest(this HttpRequest request, bool readBodyAsString = false)
{
return new ITfoxtec.Identity.Saml2.Http.HttpRequest
{
Method = request.Method,
QueryString = request.QueryString.Value,
Query = ToNameValueCollection(request.Query),
Form = "POST".Equals(request.Method, StringComparison.InvariantCultureIgnoreCase) ? ToNameValueCollection(request.Form) : null
};
}

private static NameValueCollection ToNameValueCollection(IEnumerable<KeyValuePair<string, StringValues>> items)
{
var nv = new NameValueCollection();
foreach (var item in items)
{
nv.Add(item.Key, item.Value[0]);
}
return nv;
}

//private static async Task<string> ReadBodyStringAsync(HttpRequest request)
//{
// using (var reader = new StreamReader(request.Body))
// {
// return await reader.ReadToEndAsync();
// }
//}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net6;net7</TargetFrameworks>
<Nullable>enable</Nullable>
<RootNamespace>Passingwind.Authentication.Saml2</RootNamespace>
<PackageId>Passingwind.Authentication.Saml2</PackageId>
<Authors>Passingwind</Authors>
<PackageProjectUrl>https://github.com/jxnkwlp/Passingwind.CommonLibs</PackageProjectUrl>
<RepositoryUrl>https://github.com/jxnkwlp/Passingwind.CommonLibs</RepositoryUrl>
<RepositoryType>github</RepositoryType>
<PackageTags>SAML2, authentication</PackageTags>
<Description>ASP.NET Core authentication handler for the SAML2 protocol</Description>
<PackageVersion>0.1.0</PackageVersion>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ITfoxtec.Identity.Saml2" Version="4.8.8" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions src/Passingwind.Authentication.Saml2/Saml2Defaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Passingwind.Authentication.Saml2;

public static class Saml2Defaults
{
public const string AuthenticationScheme = "Saml2";

public const string DisplayName = "Saml2";
}
146 changes: 146 additions & 0 deletions src/Passingwind.Authentication.Saml2/Saml2Events.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using ITfoxtec.Identity.Saml2;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;

namespace Passingwind.Authentication.Saml2;

public class Saml2Events : RemoteAuthenticationEvents
{
/// <summary>
/// Invoked when a protocol message is first received.
/// </summary>
public Func<MessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
/// </summary>
public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge.
/// </summary>
public Func<RedirectContext, Task> OnRedirectToIdentityProvider { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint.
/// </summary>
public Func<RemoteSignOutContext, Task> OnRemoteSignOut { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked with the security token that has been extracted from the protocol message.
/// </summary>
public Func<SecurityTokenReceivedContext, Task> OnSecurityTokenReceived { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
/// </summary>
public Func<SecurityTokenValidatedContext, Task> OnSecurityTokenValidated { get; set; } = context => Task.CompletedTask;

/// <summary>
/// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
/// </summary>
/// <param name="context"></param>
public virtual Task AuthenticationFailed(AuthenticationFailedContext context) => OnAuthenticationFailed(context);

/// <summary>
/// Invoked to manipulate redirects to the identity provider for SignIn, SignOut, or Challenge.
/// </summary>
/// <param name="context"></param>
public virtual Task RedirectToIdentityProvider(RedirectContext context) => OnRedirectToIdentityProvider(context);

/// <summary>
/// Invoked when a protocol message is first received.
/// </summary>
/// <param name="context"></param>
public virtual Task MessageReceived(MessageReceivedContext context) => OnMessageReceived(context);

/// <summary>
/// Invoked when a wsignoutcleanup request is received at the RemoteSignOutPath endpoint.
/// </summary>
/// <param name="context"></param>
public virtual Task RemoteSignOut(RemoteSignOutContext context) => OnRemoteSignOut(context);

/// <summary>
/// Invoked with the security token that has been extracted from the protocol message.
/// </summary>
/// <param name="context"></param>
public virtual Task SecurityTokenReceived(SecurityTokenReceivedContext context) => OnSecurityTokenReceived(context);

/// <summary>
/// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
/// </summary>
/// <param name="context"></param>
public virtual Task SecurityTokenValidated(SecurityTokenValidatedContext context) => OnSecurityTokenValidated(context);
}

public class RedirectContext : PropertiesContext<Saml2Options>
{
public RedirectContext(HttpContext context, AuthenticationScheme scheme, Saml2Options options, AuthenticationProperties? properties) : base(context, scheme, options, properties)
{
}

public Saml2AuthnRequest Saml2AuthnRequest { get; set; } = default!;

public Saml2RedirectBinding RedirectBinding { get; set; } = default!;

/// <summary>
/// If true, will skip any default logic for this redirect.
/// </summary>
public bool Handled { get; private set; }

/// <summary>
/// Skips any default logic for this redirect.
/// </summary>
public void HandleResponse() => Handled = true;
}

public class RemoteSignOutContext : RemoteAuthenticationContext<Saml2Options>
{
public RemoteSignOutContext(HttpContext context, AuthenticationScheme scheme, Saml2Options options, AuthenticationProperties? properties) : base(context, scheme, options, properties)
{
}

public Saml2AuthnResponse Saml2AuthnResponse { get; set; } = default!;
}

public class MessageReceivedContext : RemoteAuthenticationContext<Saml2Options>
{
public MessageReceivedContext(HttpContext context, AuthenticationScheme scheme, Saml2Options options, AuthenticationProperties? properties) : base(context, scheme, options, properties)
{
}

public Saml2AuthnResponse Saml2AuthnResponse { get; set; } = default!;
}

public class SecurityTokenReceivedContext : RemoteAuthenticationContext<Saml2Options>
{
public SecurityTokenReceivedContext(HttpContext context, AuthenticationScheme scheme, Saml2Options options, AuthenticationProperties? properties) : base(context, scheme, options, properties)
{
}

public Saml2AuthnResponse Saml2AuthnResponse { get; set; } = default!;
}

public class SecurityTokenValidatedContext : RemoteAuthenticationContext<Saml2Options>
{
public SecurityTokenValidatedContext(HttpContext context, AuthenticationScheme scheme, Saml2Options options, ClaimsPrincipal principal, AuthenticationProperties? properties) : base(context, scheme, options, properties)
{
Principal = principal;
}

public Saml2AuthnResponse Saml2AuthnResponse { get; set; } = default!;
}

public class AuthenticationFailedContext : RemoteAuthenticationContext<Saml2Options>
{
public AuthenticationFailedContext(HttpContext context, AuthenticationScheme scheme, Saml2Options options) : base(context, scheme, options, null)
{
}

public Saml2AuthnResponse Saml2AuthnResponse { get; set; } = default!;

public Exception Exception { get; set; } = default!;
}
26 changes: 26 additions & 0 deletions src/Passingwind.Authentication.Saml2/Saml2Extensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using System;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Passingwind.Authentication.Saml2;

public static class Saml2Extensions
{
public static AuthenticationBuilder AddSaml2(this AuthenticationBuilder builder, Action<Saml2Options>? configureOptions = null)
{
return AddSaml2(builder, Saml2Defaults.AuthenticationScheme, Saml2Defaults.AuthenticationScheme, configureOptions);
}

public static AuthenticationBuilder AddSaml2(this AuthenticationBuilder builder, string scheme, string? displayName = null, Action<Saml2Options>? configureOptions = null)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<Saml2Options>, Saml2PostConfigureOptions>());

builder.AddScheme<Saml2Options, Saml2Handler>(scheme, displayName, configureOptions);

builder.Services.AddTransient<Saml2Handler>();

return builder;
}
}
Loading

0 comments on commit 4e816c1

Please sign in to comment.