1
0
mirror of https://github.com/bitwarden/server synced 2026-01-07 19:13:50 +00:00

Revert filescoped (#2227)

* Revert "Add git blame entry (#2226)"

This reverts commit 239286737d.

* Revert "Turn on file scoped namespaces (#2225)"

This reverts commit 34fb4cca2a.
This commit is contained in:
Justin Baur
2022-08-29 15:53:48 -04:00
committed by GitHub
parent 239286737d
commit bae03feffe
1208 changed files with 74317 additions and 73126 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -5,50 +5,51 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Sso.Controllers;
public class HomeController : Controller
namespace Bit.Sso.Controllers
{
private readonly IIdentityServerInteractionService _interaction;
public HomeController(IIdentityServerInteractionService interaction)
public class HomeController : Controller
{
_interaction = interaction;
}
private readonly IIdentityServerInteractionService _interaction;
[Route("~/Error")]
[Route("~/Home/Error")]
[AllowAnonymous]
public async Task<IActionResult> Error(string errorId)
{
var vm = new ErrorViewModel();
// retrieve error details from identityserver
var message = string.IsNullOrWhiteSpace(errorId) ? null :
await _interaction.GetErrorContextAsync(errorId);
if (message != null)
public HomeController(IIdentityServerInteractionService interaction)
{
vm.Error = message;
_interaction = interaction;
}
else
[Route("~/Error")]
[Route("~/Home/Error")]
[AllowAnonymous]
public async Task<IActionResult> Error(string errorId)
{
vm.RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature?.Error;
if (exception is InvalidOperationException opEx && opEx.Message.Contains("schemes are: "))
var vm = new ErrorViewModel();
// retrieve error details from identityserver
var message = string.IsNullOrWhiteSpace(errorId) ? null :
await _interaction.GetErrorContextAsync(errorId);
if (message != null)
{
// Messages coming from aspnetcore with a message
// similar to "The registered sign-in schemes are: {schemes}."
// will expose other Org IDs and sign-in schemes enabled on
// the server. These errors should be truncated to just the
// scheme impacted (always the first sentence)
var cleanupPoint = opEx.Message.IndexOf(". ") + 1;
var exMessage = opEx.Message.Substring(0, cleanupPoint);
exception = new InvalidOperationException(exMessage, opEx);
vm.Error = message;
}
else
{
vm.RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier;
var exceptionHandlerPathFeature = HttpContext.Features.Get<IExceptionHandlerPathFeature>();
var exception = exceptionHandlerPathFeature?.Error;
if (exception is InvalidOperationException opEx && opEx.Message.Contains("schemes are: "))
{
// Messages coming from aspnetcore with a message
// similar to "The registered sign-in schemes are: {schemes}."
// will expose other Org IDs and sign-in schemes enabled on
// the server. These errors should be truncated to just the
// scheme impacted (always the first sentence)
var cleanupPoint = opEx.Message.IndexOf(". ") + 1;
var exMessage = opEx.Message.Substring(0, cleanupPoint);
exception = new InvalidOperationException(exMessage, opEx);
}
vm.Exception = exception;
}
vm.Exception = exception;
}
return View("Error", vm);
return View("Error", vm);
}
}
}

View File

@@ -1,20 +1,21 @@
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Sso.Controllers;
public class InfoController : Controller
namespace Bit.Sso.Controllers
{
[HttpGet("~/alive")]
[HttpGet("~/now")]
public DateTime GetAlive()
public class InfoController : Controller
{
return DateTime.UtcNow;
}
[HttpGet("~/alive")]
[HttpGet("~/now")]
public DateTime GetAlive()
{
return DateTime.UtcNow;
}
[HttpGet("~/version")]
public JsonResult GetVersion()
{
return Json(CoreHelpers.GetVersion());
[HttpGet("~/version")]
public JsonResult GetVersion()
{
return Json(CoreHelpers.GetVersion());
}
}
}

View File

@@ -5,65 +5,66 @@ using Microsoft.AspNetCore.Mvc;
using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.WebSso;
namespace Bit.Sso.Controllers;
public class MetadataController : Controller
namespace Bit.Sso.Controllers
{
private readonly IAuthenticationSchemeProvider _schemeProvider;
public MetadataController(
IAuthenticationSchemeProvider schemeProvider)
public class MetadataController : Controller
{
_schemeProvider = schemeProvider;
}
private readonly IAuthenticationSchemeProvider _schemeProvider;
[HttpGet("saml2/{scheme}")]
public async Task<IActionResult> ViewAsync(string scheme)
{
if (string.IsNullOrWhiteSpace(scheme))
public MetadataController(
IAuthenticationSchemeProvider schemeProvider)
{
return NotFound();
_schemeProvider = schemeProvider;
}
var authScheme = await _schemeProvider.GetSchemeAsync(scheme);
if (authScheme == null ||
!(authScheme is DynamicAuthenticationScheme dynamicAuthScheme) ||
dynamicAuthScheme?.SsoType != SsoType.Saml2)
[HttpGet("saml2/{scheme}")]
public async Task<IActionResult> ViewAsync(string scheme)
{
return NotFound();
if (string.IsNullOrWhiteSpace(scheme))
{
return NotFound();
}
var authScheme = await _schemeProvider.GetSchemeAsync(scheme);
if (authScheme == null ||
!(authScheme is DynamicAuthenticationScheme dynamicAuthScheme) ||
dynamicAuthScheme?.SsoType != SsoType.Saml2)
{
return NotFound();
}
if (!(dynamicAuthScheme.Options is Saml2Options options))
{
return NotFound();
}
var uri = new Uri(
Request.Scheme
+ "://"
+ Request.Host
+ Request.Path
+ Request.QueryString);
var pathBase = Request.PathBase.Value;
pathBase = string.IsNullOrEmpty(pathBase) ? "/" : pathBase;
var requestdata = new HttpRequestData(
Request.Method,
uri,
pathBase,
null,
Request.Cookies,
(data) => data);
var metadataResult = CommandFactory
.GetCommand(CommandFactory.MetadataCommand)
.Run(requestdata, options);
//Response.Headers.Add("Content-Disposition", $"filename= bitwarden-saml2-meta-{scheme}.xml");
return new ContentResult
{
Content = metadataResult.Content,
ContentType = "text/xml",
};
}
if (!(dynamicAuthScheme.Options is Saml2Options options))
{
return NotFound();
}
var uri = new Uri(
Request.Scheme
+ "://"
+ Request.Host
+ Request.Path
+ Request.QueryString);
var pathBase = Request.PathBase.Value;
pathBase = string.IsNullOrEmpty(pathBase) ? "/" : pathBase;
var requestdata = new HttpRequestData(
Request.Method,
uri,
pathBase,
null,
Request.Cookies,
(data) => data);
var metadataResult = CommandFactory
.GetCommand(CommandFactory.MetadataCommand)
.Run(requestdata, options);
//Response.Headers.Add("Content-Disposition", $"filename= bitwarden-saml2-meta-{scheme}.xml");
return new ContentResult
{
Content = metadataResult.Content,
ContentType = "text/xml",
};
}
}

View File

@@ -1,26 +1,27 @@
using IdentityServer4.Models;
namespace Bit.Sso.Models;
public class ErrorViewModel
namespace Bit.Sso.Models
{
private string _requestId;
public ErrorMessage Error { get; set; }
public Exception Exception { get; set; }
public string Message => Error?.Error;
public string Description => Error?.ErrorDescription ?? Exception?.Message;
public string RedirectUri => Error?.RedirectUri;
public string RequestId
public class ErrorViewModel
{
get
private string _requestId;
public ErrorMessage Error { get; set; }
public Exception Exception { get; set; }
public string Message => Error?.Error;
public string Description => Error?.ErrorDescription ?? Exception?.Message;
public string RedirectUri => Error?.RedirectUri;
public string RequestId
{
return Error?.RequestId ?? _requestId;
}
set
{
_requestId = value;
get
{
return Error?.RequestId ?? _requestId;
}
set
{
_requestId = value;
}
}
}
}

View File

@@ -1,6 +1,7 @@
namespace Bit.Sso.Models;
public class RedirectViewModel
namespace Bit.Sso.Models
{
public string RedirectUrl { get; set; }
public class RedirectViewModel
{
public string RedirectUrl { get; set; }
}
}

View File

@@ -1,8 +1,9 @@
using System.Security.Cryptography.X509Certificates;
namespace Bit.Sso.Models;
public class SamlEnvironment
namespace Bit.Sso.Models
{
public X509Certificate2 SpSigningCertificate { get; set; }
public class SamlEnvironment
{
public X509Certificate2 SpSigningCertificate { get; set; }
}
}

View File

@@ -1,12 +1,13 @@
using Microsoft.AspNetCore.Mvc;
namespace Bit.Sso.Models;
public class SsoPreValidateResponseModel : JsonResult
namespace Bit.Sso.Models
{
public SsoPreValidateResponseModel(string token) : base(new
public class SsoPreValidateResponseModel : JsonResult
{
token
})
{ }
public SsoPreValidateResponseModel(string token) : base(new
{
token
})
{ }
}
}

View File

@@ -2,32 +2,33 @@
using Serilog;
using Serilog.Events;
namespace Bit.Sso;
public class Program
namespace Bit.Sso
{
public static void Main(string[] args)
public class Program
{
Host
.CreateDefaultBuilder(args)
.ConfigureCustomAppConfiguration(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, e =>
public static void Main(string[] args)
{
Host
.CreateDefaultBuilder(args)
.ConfigureCustomAppConfiguration(args)
.ConfigureWebHostDefaults(webBuilder =>
{
var context = e.Properties["SourceContext"].ToString();
if (e.Properties.ContainsKey("RequestPath") &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureLogging((hostingContext, logging) =>
logging.AddSerilog(hostingContext, e =>
{
return false;
}
return e.Level >= LogEventLevel.Error;
}));
})
.Build()
.Run();
var context = e.Properties["SourceContext"].ToString();
if (e.Properties.ContainsKey("RequestPath") &&
!string.IsNullOrWhiteSpace(e.Properties["RequestPath"]?.ToString()) &&
(context.Contains(".Server.Kestrel") || context.Contains(".Core.IISHttpServer")))
{
return false;
}
return e.Level >= LogEventLevel.Error;
}));
})
.Build()
.Run();
}
}
}

View File

@@ -8,147 +8,148 @@ using IdentityServer4.Extensions;
using Microsoft.IdentityModel.Logging;
using Stripe;
namespace Bit.Sso;
public class Startup
namespace Bit.Sso
{
public Startup(IWebHostEnvironment env, IConfiguration configuration)
public class Startup
{
Configuration = configuration;
Environment = env;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; set; }
public void ConfigureServices(IServiceCollection services)
{
// Options
services.AddOptions();
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
// Stripe Billing
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
// Data Protection
services.AddCustomDataProtectionServices(Environment, globalSettings);
// Repositories
services.AddSqlServerRepositories(globalSettings);
// Context
services.AddScoped<ICurrentContext, CurrentContext>();
// Caching
services.AddMemoryCache();
services.AddDistributedCache(globalSettings);
// Mvc
services.AddControllersWithViews();
// Cookies
if (Environment.IsDevelopment())
public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
services.Configure<CookiePolicyOptions>(options =>
Configuration = configuration;
Environment = env;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment Environment { get; set; }
public void ConfigureServices(IServiceCollection services)
{
// Options
services.AddOptions();
// Settings
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
// Stripe Billing
StripeConfiguration.ApiKey = globalSettings.Stripe.ApiKey;
StripeConfiguration.MaxNetworkRetries = globalSettings.Stripe.MaxNetworkRetries;
// Data Protection
services.AddCustomDataProtectionServices(Environment, globalSettings);
// Repositories
services.AddSqlServerRepositories(globalSettings);
// Context
services.AddScoped<ICurrentContext, CurrentContext>();
// Caching
services.AddMemoryCache();
services.AddDistributedCache(globalSettings);
// Mvc
services.AddControllersWithViews();
// Cookies
if (Environment.IsDevelopment())
{
options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;
options.OnAppendCookie = ctx =>
services.Configure<CookiePolicyOptions>(options =>
{
ctx.CookieOptions.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;
};
});
options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;
options.OnAppendCookie = ctx =>
{
ctx.CookieOptions.SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;
};
});
}
// Authentication
services.AddDistributedIdentityServices(globalSettings);
services.AddAuthentication()
.AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
services.AddSsoServices(globalSettings);
// IdentityServer
services.AddSsoIdentityServerServices(Environment, globalSettings);
// Identity
services.AddCustomIdentityServices(globalSettings);
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddCoreLocalizationServices();
}
// Authentication
services.AddDistributedIdentityServices(globalSettings);
services.AddAuthentication()
.AddCookie(AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
services.AddSsoServices(globalSettings);
// IdentityServer
services.AddSsoIdentityServerServices(Environment, globalSettings);
// Identity
services.AddCustomIdentityServices(globalSettings);
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddCoreLocalizationServices();
}
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IHostApplicationLifetime appLifetime,
GlobalSettings globalSettings,
ILogger<Startup> logger)
{
if (env.IsDevelopment() || globalSettings.SelfHosted)
public void Configure(
IApplicationBuilder app,
IWebHostEnvironment env,
IHostApplicationLifetime appLifetime,
GlobalSettings globalSettings,
ILogger<Startup> logger)
{
IdentityModelEventSource.ShowPII = true;
}
app.UseSerilog(env, appLifetime, globalSettings);
// Add general security headers
app.UseMiddleware<SecurityHeadersMiddleware>();
if (!env.IsDevelopment())
{
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
app.Use(async (ctx, next) =>
if (env.IsDevelopment() || globalSettings.SelfHosted)
{
ctx.SetIdentityServerOrigin($"{uri.Scheme}://{uri.Host}");
await next();
IdentityModelEventSource.ShowPII = true;
}
app.UseSerilog(env, appLifetime, globalSettings);
// Add general security headers
app.UseMiddleware<SecurityHeadersMiddleware>();
if (!env.IsDevelopment())
{
var uri = new Uri(globalSettings.BaseServiceUri.Sso);
app.Use(async (ctx, next) =>
{
ctx.SetIdentityServerOrigin($"{uri.Scheme}://{uri.Host}");
await next();
});
}
if (globalSettings.SelfHosted)
{
app.UsePathBase("/sso");
app.UseForwardedHeaders(globalSettings);
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseCookiePolicy();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseCoreLocalization();
// Add static files to the request pipeline.
app.UseStaticFiles();
// Add routing
app.UseRouting();
// Add Cors
app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
// Add current context
app.UseMiddleware<CurrentContextMiddleware>();
// Add IdentityServer to the request pipeline.
app.UseIdentityServer(new IdentityServerMiddlewareOptions
{
AuthenticationMiddleware = app => app.UseMiddleware<SsoAuthenticationMiddleware>()
});
// Add Mvc stuff
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
// Log startup
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
}
if (globalSettings.SelfHosted)
{
app.UsePathBase("/sso");
app.UseForwardedHeaders(globalSettings);
}
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseCookiePolicy();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseCoreLocalization();
// Add static files to the request pipeline.
app.UseStaticFiles();
// Add routing
app.UseRouting();
// Add Cors
app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
// Add current context
app.UseMiddleware<CurrentContextMiddleware>();
// Add IdentityServer to the request pipeline.
app.UseIdentityServer(new IdentityServerMiddlewareOptions
{
AuthenticationMiddleware = app => app.UseMiddleware<SsoAuthenticationMiddleware>()
});
// Add Mvc stuff
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
// Log startup
logger.LogInformation(Constants.BypassFiltersEventId, globalSettings.ProjectName + " started.");
}
}

View File

@@ -1,45 +1,46 @@
using System.Security.Claims;
using System.Text.RegularExpressions;
namespace Bit.Sso.Utilities;
public static class ClaimsExtensions
namespace Bit.Sso.Utilities
{
private static readonly Regex _normalizeTextRegEx =
new Regex(@"[^a-zA-Z]", RegexOptions.CultureInvariant | RegexOptions.Singleline);
public static string GetFirstMatch(this IEnumerable<Claim> claims, params string[] possibleNames)
public static class ClaimsExtensions
{
var normalizedClaims = claims.Select(c => (Normalize(c.Type), c.Value)).ToList();
private static readonly Regex _normalizeTextRegEx =
new Regex(@"[^a-zA-Z]", RegexOptions.CultureInvariant | RegexOptions.Singleline);
// Order of prescendence is by passed in names
foreach (var name in possibleNames.Select(Normalize))
public static string GetFirstMatch(this IEnumerable<Claim> claims, params string[] possibleNames)
{
// Second by order of claims (find claim by name)
foreach (var claim in normalizedClaims)
var normalizedClaims = claims.Select(c => (Normalize(c.Type), c.Value)).ToList();
// Order of prescendence is by passed in names
foreach (var name in possibleNames.Select(Normalize))
{
if (Equals(claim.Item1, name))
// Second by order of claims (find claim by name)
foreach (var claim in normalizedClaims)
{
return claim.Value;
if (Equals(claim.Item1, name))
{
return claim.Value;
}
}
}
return null;
}
return null;
}
private static bool Equals(string text, string compare)
{
return text == compare ||
(string.IsNullOrWhiteSpace(text) && string.IsNullOrWhiteSpace(compare)) ||
string.Equals(Normalize(text), compare, StringComparison.InvariantCultureIgnoreCase);
}
private static string Normalize(string text)
{
if (string.IsNullOrWhiteSpace(text))
private static bool Equals(string text, string compare)
{
return text;
return text == compare ||
(string.IsNullOrWhiteSpace(text) && string.IsNullOrWhiteSpace(compare)) ||
string.Equals(Normalize(text), compare, StringComparison.InvariantCultureIgnoreCase);
}
private static string Normalize(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return text;
}
return _normalizeTextRegEx.Replace(text, string.Empty);
}
return _normalizeTextRegEx.Replace(text, string.Empty);
}
}

View File

@@ -5,31 +5,32 @@ using IdentityServer4.Services;
using IdentityServer4.Stores;
using IdentityServer4.Validation;
namespace Bit.Sso.Utilities;
public class DiscoveryResponseGenerator : IdentityServer4.ResponseHandling.DiscoveryResponseGenerator
namespace Bit.Sso.Utilities
{
private readonly GlobalSettings _globalSettings;
public DiscoveryResponseGenerator(
IdentityServerOptions options,
IResourceStore resourceStore,
IKeyMaterialService keys,
ExtensionGrantValidator extensionGrants,
ISecretsListParser secretParsers,
IResourceOwnerPasswordValidator resourceOwnerValidator,
ILogger<DiscoveryResponseGenerator> logger,
GlobalSettings globalSettings)
: base(options, resourceStore, keys, extensionGrants, secretParsers, resourceOwnerValidator, logger)
public class DiscoveryResponseGenerator : IdentityServer4.ResponseHandling.DiscoveryResponseGenerator
{
_globalSettings = globalSettings;
}
private readonly GlobalSettings _globalSettings;
public override async Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(
string baseUrl, string issuerUri)
{
var dict = await base.CreateDiscoveryDocumentAsync(baseUrl, issuerUri);
return CoreHelpers.AdjustIdentityServerConfig(dict, _globalSettings.BaseServiceUri.Sso,
_globalSettings.BaseServiceUri.InternalSso);
public DiscoveryResponseGenerator(
IdentityServerOptions options,
IResourceStore resourceStore,
IKeyMaterialService keys,
ExtensionGrantValidator extensionGrants,
ISecretsListParser secretParsers,
IResourceOwnerPasswordValidator resourceOwnerValidator,
ILogger<DiscoveryResponseGenerator> logger,
GlobalSettings globalSettings)
: base(options, resourceStore, keys, extensionGrants, secretParsers, resourceOwnerValidator, logger)
{
_globalSettings = globalSettings;
}
public override async Task<Dictionary<string, object>> CreateDiscoveryDocumentAsync(
string baseUrl, string issuerUri)
{
var dict = await base.CreateDiscoveryDocumentAsync(baseUrl, issuerUri);
return CoreHelpers.AdjustIdentityServerConfig(dict, _globalSettings.BaseServiceUri.Sso,
_globalSettings.BaseServiceUri.InternalSso);
}
}
}

View File

@@ -3,87 +3,88 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Sustainsys.Saml2.AspNetCore2;
namespace Bit.Sso.Utilities;
public class DynamicAuthenticationScheme : AuthenticationScheme, IDynamicAuthenticationScheme
namespace Bit.Sso.Utilities
{
public DynamicAuthenticationScheme(string name, string displayName, Type handlerType,
AuthenticationSchemeOptions options)
: base(name, displayName, handlerType)
public class DynamicAuthenticationScheme : AuthenticationScheme, IDynamicAuthenticationScheme
{
Options = options;
}
public DynamicAuthenticationScheme(string name, string displayName, Type handlerType,
AuthenticationSchemeOptions options, SsoType ssoType)
: this(name, displayName, handlerType, options)
{
SsoType = ssoType;
}
public DynamicAuthenticationScheme(string name, string displayName, Type handlerType,
AuthenticationSchemeOptions options)
: base(name, displayName, handlerType)
{
Options = options;
}
public DynamicAuthenticationScheme(string name, string displayName, Type handlerType,
AuthenticationSchemeOptions options, SsoType ssoType)
: this(name, displayName, handlerType, options)
{
SsoType = ssoType;
}
public AuthenticationSchemeOptions Options { get; set; }
public SsoType SsoType { get; set; }
public AuthenticationSchemeOptions Options { get; set; }
public SsoType SsoType { get; set; }
public async Task Validate()
{
switch (SsoType)
public async Task Validate()
{
case SsoType.OpenIdConnect:
await ValidateOpenIdConnectAsync();
break;
case SsoType.Saml2:
ValidateSaml();
break;
default:
break;
}
}
private void ValidateSaml()
{
if (SsoType != SsoType.Saml2)
{
return;
}
if (!(Options is Saml2Options samlOptions))
{
throw new Exception("InvalidAuthenticationOptionsForSaml2SchemeError");
}
samlOptions.Validate(Name);
}
private async Task ValidateOpenIdConnectAsync()
{
if (SsoType != SsoType.OpenIdConnect)
{
return;
}
if (!(Options is OpenIdConnectOptions oidcOptions))
{
throw new Exception("InvalidAuthenticationOptionsForOidcSchemeError");
}
oidcOptions.Validate();
if (oidcOptions.Configuration == null)
{
if (oidcOptions.ConfigurationManager == null)
switch (SsoType)
{
throw new Exception("PostConfigurationNotExecutedError");
case SsoType.OpenIdConnect:
await ValidateOpenIdConnectAsync();
break;
case SsoType.Saml2:
ValidateSaml();
break;
default:
break;
}
}
private void ValidateSaml()
{
if (SsoType != SsoType.Saml2)
{
return;
}
if (!(Options is Saml2Options samlOptions))
{
throw new Exception("InvalidAuthenticationOptionsForSaml2SchemeError");
}
samlOptions.Validate(Name);
}
private async Task ValidateOpenIdConnectAsync()
{
if (SsoType != SsoType.OpenIdConnect)
{
return;
}
if (!(Options is OpenIdConnectOptions oidcOptions))
{
throw new Exception("InvalidAuthenticationOptionsForOidcSchemeError");
}
oidcOptions.Validate();
if (oidcOptions.Configuration == null)
{
if (oidcOptions.ConfigurationManager == null)
{
throw new Exception("PostConfigurationNotExecutedError");
}
if (oidcOptions.Configuration == null)
{
try
{
oidcOptions.Configuration = await oidcOptions.ConfigurationManager
.GetConfigurationAsync(CancellationToken.None);
}
catch (Exception ex)
{
throw new Exception("ReadingOpenIdConnectMetadataFailedError", ex);
}
}
}
if (oidcOptions.Configuration == null)
{
try
{
oidcOptions.Configuration = await oidcOptions.ConfigurationManager
.GetConfigurationAsync(CancellationToken.None);
}
catch (Exception ex)
{
throw new Exception("ReadingOpenIdConnectMetadataFailedError", ex);
}
throw new Exception("NoOpenIdConnectMetadataError");
}
}
if (oidcOptions.Configuration == null)
{
throw new Exception("NoOpenIdConnectMetadataError");
}
}
}

View File

@@ -18,440 +18,441 @@ using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.Configuration;
using Sustainsys.Saml2.Saml2P;
namespace Bit.Core.Business.Sso;
public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
namespace Bit.Core.Business.Sso
{
private readonly IPostConfigureOptions<OpenIdConnectOptions> _oidcPostConfigureOptions;
private readonly IExtendedOptionsMonitorCache<OpenIdConnectOptions> _extendedOidcOptionsMonitorCache;
private readonly IPostConfigureOptions<Saml2Options> _saml2PostConfigureOptions;
private readonly IExtendedOptionsMonitorCache<Saml2Options> _extendedSaml2OptionsMonitorCache;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings;
private readonly SamlEnvironment _samlEnvironment;
private readonly TimeSpan _schemeCacheLifetime;
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedSchemes;
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedHandlerSchemes;
private readonly SemaphoreSlim _semaphore;
private readonly IHttpContextAccessor _httpContextAccessor;
private DateTime? _lastSchemeLoad;
private IEnumerable<DynamicAuthenticationScheme> _schemesCopy = Array.Empty<DynamicAuthenticationScheme>();
private IEnumerable<DynamicAuthenticationScheme> _handlerSchemesCopy = Array.Empty<DynamicAuthenticationScheme>();
public DynamicAuthenticationSchemeProvider(
IOptions<AuthenticationOptions> options,
IPostConfigureOptions<OpenIdConnectOptions> oidcPostConfigureOptions,
IOptionsMonitorCache<OpenIdConnectOptions> oidcOptionsMonitorCache,
IPostConfigureOptions<Saml2Options> saml2PostConfigureOptions,
IOptionsMonitorCache<Saml2Options> saml2OptionsMonitorCache,
ISsoConfigRepository ssoConfigRepository,
ILogger<DynamicAuthenticationSchemeProvider> logger,
GlobalSettings globalSettings,
SamlEnvironment samlEnvironment,
IHttpContextAccessor httpContextAccessor)
: base(options)
public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
{
_oidcPostConfigureOptions = oidcPostConfigureOptions;
_extendedOidcOptionsMonitorCache = oidcOptionsMonitorCache as
IExtendedOptionsMonitorCache<OpenIdConnectOptions>;
if (_extendedOidcOptionsMonitorCache == null)
private readonly IPostConfigureOptions<OpenIdConnectOptions> _oidcPostConfigureOptions;
private readonly IExtendedOptionsMonitorCache<OpenIdConnectOptions> _extendedOidcOptionsMonitorCache;
private readonly IPostConfigureOptions<Saml2Options> _saml2PostConfigureOptions;
private readonly IExtendedOptionsMonitorCache<Saml2Options> _extendedSaml2OptionsMonitorCache;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ILogger _logger;
private readonly GlobalSettings _globalSettings;
private readonly SamlEnvironment _samlEnvironment;
private readonly TimeSpan _schemeCacheLifetime;
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedSchemes;
private readonly Dictionary<string, DynamicAuthenticationScheme> _cachedHandlerSchemes;
private readonly SemaphoreSlim _semaphore;
private readonly IHttpContextAccessor _httpContextAccessor;
private DateTime? _lastSchemeLoad;
private IEnumerable<DynamicAuthenticationScheme> _schemesCopy = Array.Empty<DynamicAuthenticationScheme>();
private IEnumerable<DynamicAuthenticationScheme> _handlerSchemesCopy = Array.Empty<DynamicAuthenticationScheme>();
public DynamicAuthenticationSchemeProvider(
IOptions<AuthenticationOptions> options,
IPostConfigureOptions<OpenIdConnectOptions> oidcPostConfigureOptions,
IOptionsMonitorCache<OpenIdConnectOptions> oidcOptionsMonitorCache,
IPostConfigureOptions<Saml2Options> saml2PostConfigureOptions,
IOptionsMonitorCache<Saml2Options> saml2OptionsMonitorCache,
ISsoConfigRepository ssoConfigRepository,
ILogger<DynamicAuthenticationSchemeProvider> logger,
GlobalSettings globalSettings,
SamlEnvironment samlEnvironment,
IHttpContextAccessor httpContextAccessor)
: base(options)
{
throw new ArgumentNullException("_extendedOidcOptionsMonitorCache could not be resolved.");
_oidcPostConfigureOptions = oidcPostConfigureOptions;
_extendedOidcOptionsMonitorCache = oidcOptionsMonitorCache as
IExtendedOptionsMonitorCache<OpenIdConnectOptions>;
if (_extendedOidcOptionsMonitorCache == null)
{
throw new ArgumentNullException("_extendedOidcOptionsMonitorCache could not be resolved.");
}
_saml2PostConfigureOptions = saml2PostConfigureOptions;
_extendedSaml2OptionsMonitorCache = saml2OptionsMonitorCache as
IExtendedOptionsMonitorCache<Saml2Options>;
if (_extendedSaml2OptionsMonitorCache == null)
{
throw new ArgumentNullException("_extendedSaml2OptionsMonitorCache could not be resolved.");
}
_ssoConfigRepository = ssoConfigRepository;
_logger = logger;
_globalSettings = globalSettings;
_schemeCacheLifetime = TimeSpan.FromSeconds(_globalSettings.Sso?.CacheLifetimeInSeconds ?? 30);
_samlEnvironment = samlEnvironment;
_cachedSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_cachedHandlerSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_semaphore = new SemaphoreSlim(1);
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
_saml2PostConfigureOptions = saml2PostConfigureOptions;
_extendedSaml2OptionsMonitorCache = saml2OptionsMonitorCache as
IExtendedOptionsMonitorCache<Saml2Options>;
if (_extendedSaml2OptionsMonitorCache == null)
private bool CacheIsValid
{
throw new ArgumentNullException("_extendedSaml2OptionsMonitorCache could not be resolved.");
get => _lastSchemeLoad.HasValue
&& _lastSchemeLoad.Value.Add(_schemeCacheLifetime) >= DateTime.UtcNow;
}
_ssoConfigRepository = ssoConfigRepository;
_logger = logger;
_globalSettings = globalSettings;
_schemeCacheLifetime = TimeSpan.FromSeconds(_globalSettings.Sso?.CacheLifetimeInSeconds ?? 30);
_samlEnvironment = samlEnvironment;
_cachedSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_cachedHandlerSchemes = new Dictionary<string, DynamicAuthenticationScheme>();
_semaphore = new SemaphoreSlim(1);
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
}
private bool CacheIsValid
{
get => _lastSchemeLoad.HasValue
&& _lastSchemeLoad.Value.Add(_schemeCacheLifetime) >= DateTime.UtcNow;
}
public override async Task<AuthenticationScheme> GetSchemeAsync(string name)
{
var scheme = await base.GetSchemeAsync(name);
if (scheme != null)
public override async Task<AuthenticationScheme> GetSchemeAsync(string name)
{
return scheme;
var scheme = await base.GetSchemeAsync(name);
if (scheme != null)
{
return scheme;
}
try
{
var dynamicScheme = await GetDynamicSchemeAsync(name);
return dynamicScheme;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to load a dynamic authentication scheme for '{0}'", name);
}
return null;
}
try
public override async Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
{
var dynamicScheme = await GetDynamicSchemeAsync(name);
return dynamicScheme;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unable to load a dynamic authentication scheme for '{0}'", name);
var existingSchemes = await base.GetAllSchemesAsync();
var schemes = new List<AuthenticationScheme>();
schemes.AddRange(existingSchemes);
await LoadAllDynamicSchemesIntoCacheAsync();
schemes.AddRange(_schemesCopy);
return schemes.ToArray();
}
return null;
}
public override async Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
{
var existingSchemes = await base.GetAllSchemesAsync();
var schemes = new List<AuthenticationScheme>();
schemes.AddRange(existingSchemes);
await LoadAllDynamicSchemesIntoCacheAsync();
schemes.AddRange(_schemesCopy);
return schemes.ToArray();
}
public override async Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
{
var existingSchemes = await base.GetRequestHandlerSchemesAsync();
var schemes = new List<AuthenticationScheme>();
schemes.AddRange(existingSchemes);
await LoadAllDynamicSchemesIntoCacheAsync();
schemes.AddRange(_handlerSchemesCopy);
return schemes.ToArray();
}
private async Task LoadAllDynamicSchemesIntoCacheAsync()
{
if (CacheIsValid)
public override async Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
{
// Our cache hasn't expired or been invalidated, ignore request
return;
var existingSchemes = await base.GetRequestHandlerSchemesAsync();
var schemes = new List<AuthenticationScheme>();
schemes.AddRange(existingSchemes);
await LoadAllDynamicSchemesIntoCacheAsync();
schemes.AddRange(_handlerSchemesCopy);
return schemes.ToArray();
}
await _semaphore.WaitAsync();
try
private async Task LoadAllDynamicSchemesIntoCacheAsync()
{
if (CacheIsValid)
{
// Just in case (double-checked locking pattern)
// Our cache hasn't expired or been invalidated, ignore request
return;
}
// Save time just in case the following operation takes longer
var now = DateTime.UtcNow;
var newSchemes = await _ssoConfigRepository.GetManyByRevisionNotBeforeDate(_lastSchemeLoad);
foreach (var config in newSchemes)
await _semaphore.WaitAsync();
try
{
DynamicAuthenticationScheme scheme;
try
if (CacheIsValid)
{
scheme = GetSchemeFromSsoConfig(config);
// Just in case (double-checked locking pattern)
return;
}
catch (Exception ex)
// Save time just in case the following operation takes longer
var now = DateTime.UtcNow;
var newSchemes = await _ssoConfigRepository.GetManyByRevisionNotBeforeDate(_lastSchemeLoad);
foreach (var config in newSchemes)
{
_logger.LogError(ex, "Error converting configuration to scheme for '{0}'", config.Id);
continue;
DynamicAuthenticationScheme scheme;
try
{
scheme = GetSchemeFromSsoConfig(config);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error converting configuration to scheme for '{0}'", config.Id);
continue;
}
if (scheme == null)
{
continue;
}
SetSchemeInCache(scheme);
}
if (scheme == null)
if (newSchemes.Any())
{
continue;
// Maintain "safe" copy for use in enumeration routines
_schemesCopy = _cachedSchemes.Values.ToArray();
_handlerSchemesCopy = _cachedHandlerSchemes.Values.ToArray();
}
SetSchemeInCache(scheme);
_lastSchemeLoad = now;
}
finally
{
_semaphore.Release();
}
}
private DynamicAuthenticationScheme SetSchemeInCache(DynamicAuthenticationScheme scheme)
{
if (!PostConfigureDynamicScheme(scheme))
{
return null;
}
_cachedSchemes[scheme.Name] = scheme;
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
{
_cachedHandlerSchemes[scheme.Name] = scheme;
}
return scheme;
}
private async Task<DynamicAuthenticationScheme> GetDynamicSchemeAsync(string name)
{
if (_cachedSchemes.TryGetValue(name, out var cachedScheme))
{
return cachedScheme;
}
if (newSchemes.Any())
{
// Maintain "safe" copy for use in enumeration routines
_schemesCopy = _cachedSchemes.Values.ToArray();
_handlerSchemesCopy = _cachedHandlerSchemes.Values.ToArray();
}
_lastSchemeLoad = now;
}
finally
{
_semaphore.Release();
}
}
private DynamicAuthenticationScheme SetSchemeInCache(DynamicAuthenticationScheme scheme)
{
if (!PostConfigureDynamicScheme(scheme))
{
return null;
}
_cachedSchemes[scheme.Name] = scheme;
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
{
_cachedHandlerSchemes[scheme.Name] = scheme;
}
return scheme;
}
private async Task<DynamicAuthenticationScheme> GetDynamicSchemeAsync(string name)
{
if (_cachedSchemes.TryGetValue(name, out var cachedScheme))
{
return cachedScheme;
}
var scheme = await GetSchemeFromSsoConfigAsync(name);
if (scheme == null)
{
return null;
}
await _semaphore.WaitAsync();
try
{
scheme = SetSchemeInCache(scheme);
var scheme = await GetSchemeFromSsoConfigAsync(name);
if (scheme == null)
{
return null;
}
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
await _semaphore.WaitAsync();
try
{
_handlerSchemesCopy = _cachedHandlerSchemes.Values.ToArray();
scheme = SetSchemeInCache(scheme);
if (scheme == null)
{
return null;
}
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
{
_handlerSchemesCopy = _cachedHandlerSchemes.Values.ToArray();
}
_schemesCopy = _cachedSchemes.Values.ToArray();
}
_schemesCopy = _cachedSchemes.Values.ToArray();
}
finally
{
// Note: _lastSchemeLoad is not set here, this is a one-off
// and should not impact loading further cache updates
_semaphore.Release();
}
return scheme;
}
private bool PostConfigureDynamicScheme(DynamicAuthenticationScheme scheme)
{
try
{
if (scheme.SsoType == SsoType.OpenIdConnect && scheme.Options is OpenIdConnectOptions oidcOptions)
finally
{
_oidcPostConfigureOptions.PostConfigure(scheme.Name, oidcOptions);
_extendedOidcOptionsMonitorCache.AddOrUpdate(scheme.Name, oidcOptions);
// Note: _lastSchemeLoad is not set here, this is a one-off
// and should not impact loading further cache updates
_semaphore.Release();
}
else if (scheme.SsoType == SsoType.Saml2 && scheme.Options is Saml2Options saml2Options)
return scheme;
}
private bool PostConfigureDynamicScheme(DynamicAuthenticationScheme scheme)
{
try
{
_saml2PostConfigureOptions.PostConfigure(scheme.Name, saml2Options);
_extendedSaml2OptionsMonitorCache.AddOrUpdate(scheme.Name, saml2Options);
if (scheme.SsoType == SsoType.OpenIdConnect && scheme.Options is OpenIdConnectOptions oidcOptions)
{
_oidcPostConfigureOptions.PostConfigure(scheme.Name, oidcOptions);
_extendedOidcOptionsMonitorCache.AddOrUpdate(scheme.Name, oidcOptions);
}
else if (scheme.SsoType == SsoType.Saml2 && scheme.Options is Saml2Options saml2Options)
{
_saml2PostConfigureOptions.PostConfigure(scheme.Name, saml2Options);
_extendedSaml2OptionsMonitorCache.AddOrUpdate(scheme.Name, saml2Options);
}
return true;
}
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error performing post configuration for '{0}' ({1})",
scheme.Name, scheme.DisplayName);
}
return false;
}
private DynamicAuthenticationScheme GetSchemeFromSsoConfig(SsoConfig config)
{
var data = config.GetData();
return data.ConfigType switch
{
SsoType.OpenIdConnect => GetOidcAuthenticationScheme(config.OrganizationId.ToString(), data),
SsoType.Saml2 => GetSaml2AuthenticationScheme(config.OrganizationId.ToString(), data),
_ => throw new Exception($"SSO Config Type, '{data.ConfigType}', not supported"),
};
}
private async Task<DynamicAuthenticationScheme> GetSchemeFromSsoConfigAsync(string name)
{
if (!Guid.TryParse(name, out var organizationId))
{
_logger.LogWarning("Could not determine organization id from name, '{0}'", name);
return null;
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
if (ssoConfig == null || !ssoConfig.Enabled)
{
_logger.LogWarning("Could not find SSO config or config was not enabled for '{0}'", name);
return null;
}
return GetSchemeFromSsoConfig(ssoConfig);
}
private DynamicAuthenticationScheme GetOidcAuthenticationScheme(string name, SsoConfigurationData config)
{
var oidcOptions = new OpenIdConnectOptions
{
Authority = config.Authority,
ClientId = config.ClientId,
ClientSecret = config.ClientSecret,
ResponseType = "code",
ResponseMode = "form_post",
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
SignOutScheme = IdentityServerConstants.SignoutScheme,
SaveTokens = false, // reduce overall request size
TokenValidationParameters = new TokenValidationParameters
catch (Exception ex)
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
},
CallbackPath = SsoConfigurationData.BuildCallbackPath(),
SignedOutCallbackPath = SsoConfigurationData.BuildSignedOutCallbackPath(),
MetadataAddress = config.MetadataAddress,
// Prevents URLs that go beyond 1024 characters which may break for some servers
AuthenticationMethod = config.RedirectBehavior,
GetClaimsFromUserInfoEndpoint = config.GetClaimsFromUserInfoEndpoint,
};
oidcOptions.Scope
.AddIfNotExists(OpenIdConnectScopes.OpenId)
.AddIfNotExists(OpenIdConnectScopes.Email)
.AddIfNotExists(OpenIdConnectScopes.Profile);
foreach (var scope in config.GetAdditionalScopes())
{
oidcOptions.Scope.AddIfNotExists(scope);
}
if (!string.IsNullOrWhiteSpace(config.ExpectedReturnAcrValue))
{
oidcOptions.Scope.AddIfNotExists(OpenIdConnectScopes.Acr);
_logger.LogError(ex, "Error performing post configuration for '{0}' ({1})",
scheme.Name, scheme.DisplayName);
}
return false;
}
oidcOptions.StateDataFormat = new DistributedCacheStateDataFormatter(_httpContextAccessor, name);
// see: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest (acr_values)
if (!string.IsNullOrWhiteSpace(config.AcrValues))
private DynamicAuthenticationScheme GetSchemeFromSsoConfig(SsoConfig config)
{
oidcOptions.Events ??= new OpenIdConnectEvents();
oidcOptions.Events.OnRedirectToIdentityProvider = ctx =>
var data = config.GetData();
return data.ConfigType switch
{
ctx.ProtocolMessage.AcrValues = config.AcrValues;
return Task.CompletedTask;
SsoType.OpenIdConnect => GetOidcAuthenticationScheme(config.OrganizationId.ToString(), data),
SsoType.Saml2 => GetSaml2AuthenticationScheme(config.OrganizationId.ToString(), data),
_ => throw new Exception($"SSO Config Type, '{data.ConfigType}', not supported"),
};
}
return new DynamicAuthenticationScheme(name, name, typeof(OpenIdConnectHandler),
oidcOptions, SsoType.OpenIdConnect);
}
private DynamicAuthenticationScheme GetSaml2AuthenticationScheme(string name, SsoConfigurationData config)
{
if (_samlEnvironment == null)
private async Task<DynamicAuthenticationScheme> GetSchemeFromSsoConfigAsync(string name)
{
throw new Exception($"SSO SAML2 Service Provider profile is missing for {name}");
if (!Guid.TryParse(name, out var organizationId))
{
_logger.LogWarning("Could not determine organization id from name, '{0}'", name);
return null;
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
if (ssoConfig == null || !ssoConfig.Enabled)
{
_logger.LogWarning("Could not find SSO config or config was not enabled for '{0}'", name);
return null;
}
return GetSchemeFromSsoConfig(ssoConfig);
}
var spEntityId = new Sustainsys.Saml2.Metadata.EntityId(
SsoConfigurationData.BuildSaml2ModulePath(_globalSettings.BaseServiceUri.Sso));
bool? allowCreate = null;
if (config.SpNameIdFormat != Saml2NameIdFormat.Transient)
private DynamicAuthenticationScheme GetOidcAuthenticationScheme(string name, SsoConfigurationData config)
{
allowCreate = true;
}
var spOptions = new SPOptions
{
EntityId = spEntityId,
ModulePath = SsoConfigurationData.BuildSaml2ModulePath(null, name),
NameIdPolicy = new Saml2NameIdPolicy(allowCreate, GetNameIdFormat(config.SpNameIdFormat)),
WantAssertionsSigned = config.SpWantAssertionsSigned,
AuthenticateRequestSigningBehavior = GetSigningBehavior(config.SpSigningBehavior),
ValidateCertificates = config.SpValidateCertificates,
};
if (!string.IsNullOrWhiteSpace(config.SpMinIncomingSigningAlgorithm))
{
spOptions.MinIncomingSigningAlgorithm = config.SpMinIncomingSigningAlgorithm;
}
if (!string.IsNullOrWhiteSpace(config.SpOutboundSigningAlgorithm))
{
spOptions.OutboundSigningAlgorithm = config.SpOutboundSigningAlgorithm;
}
if (_samlEnvironment.SpSigningCertificate != null)
{
spOptions.ServiceCertificates.Add(_samlEnvironment.SpSigningCertificate);
var oidcOptions = new OpenIdConnectOptions
{
Authority = config.Authority,
ClientId = config.ClientId,
ClientSecret = config.ClientSecret,
ResponseType = "code",
ResponseMode = "form_post",
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
SignOutScheme = IdentityServerConstants.SignoutScheme,
SaveTokens = false, // reduce overall request size
TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = JwtClaimTypes.Name,
RoleClaimType = JwtClaimTypes.Role,
},
CallbackPath = SsoConfigurationData.BuildCallbackPath(),
SignedOutCallbackPath = SsoConfigurationData.BuildSignedOutCallbackPath(),
MetadataAddress = config.MetadataAddress,
// Prevents URLs that go beyond 1024 characters which may break for some servers
AuthenticationMethod = config.RedirectBehavior,
GetClaimsFromUserInfoEndpoint = config.GetClaimsFromUserInfoEndpoint,
};
oidcOptions.Scope
.AddIfNotExists(OpenIdConnectScopes.OpenId)
.AddIfNotExists(OpenIdConnectScopes.Email)
.AddIfNotExists(OpenIdConnectScopes.Profile);
foreach (var scope in config.GetAdditionalScopes())
{
oidcOptions.Scope.AddIfNotExists(scope);
}
if (!string.IsNullOrWhiteSpace(config.ExpectedReturnAcrValue))
{
oidcOptions.Scope.AddIfNotExists(OpenIdConnectScopes.Acr);
}
oidcOptions.StateDataFormat = new DistributedCacheStateDataFormatter(_httpContextAccessor, name);
// see: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest (acr_values)
if (!string.IsNullOrWhiteSpace(config.AcrValues))
{
oidcOptions.Events ??= new OpenIdConnectEvents();
oidcOptions.Events.OnRedirectToIdentityProvider = ctx =>
{
ctx.ProtocolMessage.AcrValues = config.AcrValues;
return Task.CompletedTask;
};
}
return new DynamicAuthenticationScheme(name, name, typeof(OpenIdConnectHandler),
oidcOptions, SsoType.OpenIdConnect);
}
var idpEntityId = new Sustainsys.Saml2.Metadata.EntityId(config.IdpEntityId);
var idp = new Sustainsys.Saml2.IdentityProvider(idpEntityId, spOptions)
private DynamicAuthenticationScheme GetSaml2AuthenticationScheme(string name, SsoConfigurationData config)
{
Binding = GetBindingType(config.IdpBindingType),
AllowUnsolicitedAuthnResponse = config.IdpAllowUnsolicitedAuthnResponse,
DisableOutboundLogoutRequests = config.IdpDisableOutboundLogoutRequests,
WantAuthnRequestsSigned = config.IdpWantAuthnRequestsSigned,
};
if (!string.IsNullOrWhiteSpace(config.IdpSingleSignOnServiceUrl))
{
idp.SingleSignOnServiceUrl = new Uri(config.IdpSingleSignOnServiceUrl);
if (_samlEnvironment == null)
{
throw new Exception($"SSO SAML2 Service Provider profile is missing for {name}");
}
var spEntityId = new Sustainsys.Saml2.Metadata.EntityId(
SsoConfigurationData.BuildSaml2ModulePath(_globalSettings.BaseServiceUri.Sso));
bool? allowCreate = null;
if (config.SpNameIdFormat != Saml2NameIdFormat.Transient)
{
allowCreate = true;
}
var spOptions = new SPOptions
{
EntityId = spEntityId,
ModulePath = SsoConfigurationData.BuildSaml2ModulePath(null, name),
NameIdPolicy = new Saml2NameIdPolicy(allowCreate, GetNameIdFormat(config.SpNameIdFormat)),
WantAssertionsSigned = config.SpWantAssertionsSigned,
AuthenticateRequestSigningBehavior = GetSigningBehavior(config.SpSigningBehavior),
ValidateCertificates = config.SpValidateCertificates,
};
if (!string.IsNullOrWhiteSpace(config.SpMinIncomingSigningAlgorithm))
{
spOptions.MinIncomingSigningAlgorithm = config.SpMinIncomingSigningAlgorithm;
}
if (!string.IsNullOrWhiteSpace(config.SpOutboundSigningAlgorithm))
{
spOptions.OutboundSigningAlgorithm = config.SpOutboundSigningAlgorithm;
}
if (_samlEnvironment.SpSigningCertificate != null)
{
spOptions.ServiceCertificates.Add(_samlEnvironment.SpSigningCertificate);
}
var idpEntityId = new Sustainsys.Saml2.Metadata.EntityId(config.IdpEntityId);
var idp = new Sustainsys.Saml2.IdentityProvider(idpEntityId, spOptions)
{
Binding = GetBindingType(config.IdpBindingType),
AllowUnsolicitedAuthnResponse = config.IdpAllowUnsolicitedAuthnResponse,
DisableOutboundLogoutRequests = config.IdpDisableOutboundLogoutRequests,
WantAuthnRequestsSigned = config.IdpWantAuthnRequestsSigned,
};
if (!string.IsNullOrWhiteSpace(config.IdpSingleSignOnServiceUrl))
{
idp.SingleSignOnServiceUrl = new Uri(config.IdpSingleSignOnServiceUrl);
}
if (!string.IsNullOrWhiteSpace(config.IdpSingleLogoutServiceUrl))
{
idp.SingleLogoutServiceUrl = new Uri(config.IdpSingleLogoutServiceUrl);
}
if (!string.IsNullOrWhiteSpace(config.IdpOutboundSigningAlgorithm))
{
idp.OutboundSigningAlgorithm = config.IdpOutboundSigningAlgorithm;
}
if (!string.IsNullOrWhiteSpace(config.IdpX509PublicCert))
{
var cert = CoreHelpers.Base64UrlDecode(config.IdpX509PublicCert);
idp.SigningKeys.AddConfiguredKey(new X509Certificate2(cert));
}
idp.ArtifactResolutionServiceUrls.Clear();
// This must happen last since it calls Validate() internally.
idp.LoadMetadata = false;
var options = new Saml2Options
{
SPOptions = spOptions,
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,
CookieManager = new IdentityServer.DistributedCacheCookieManager(),
};
options.IdentityProviders.Add(idp);
return new DynamicAuthenticationScheme(name, name, typeof(Saml2Handler), options, SsoType.Saml2);
}
if (!string.IsNullOrWhiteSpace(config.IdpSingleLogoutServiceUrl))
private NameIdFormat GetNameIdFormat(Saml2NameIdFormat format)
{
idp.SingleLogoutServiceUrl = new Uri(config.IdpSingleLogoutServiceUrl);
return format switch
{
Saml2NameIdFormat.Unspecified => NameIdFormat.Unspecified,
Saml2NameIdFormat.EmailAddress => NameIdFormat.EmailAddress,
Saml2NameIdFormat.X509SubjectName => NameIdFormat.X509SubjectName,
Saml2NameIdFormat.WindowsDomainQualifiedName => NameIdFormat.WindowsDomainQualifiedName,
Saml2NameIdFormat.KerberosPrincipalName => NameIdFormat.KerberosPrincipalName,
Saml2NameIdFormat.EntityIdentifier => NameIdFormat.EntityIdentifier,
Saml2NameIdFormat.Persistent => NameIdFormat.Persistent,
Saml2NameIdFormat.Transient => NameIdFormat.Transient,
_ => NameIdFormat.NotConfigured,
};
}
if (!string.IsNullOrWhiteSpace(config.IdpOutboundSigningAlgorithm))
private SigningBehavior GetSigningBehavior(Saml2SigningBehavior behavior)
{
idp.OutboundSigningAlgorithm = config.IdpOutboundSigningAlgorithm;
return behavior switch
{
Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned => SigningBehavior.IfIdpWantAuthnRequestsSigned,
Saml2SigningBehavior.Always => SigningBehavior.Always,
Saml2SigningBehavior.Never => SigningBehavior.Never,
_ => SigningBehavior.IfIdpWantAuthnRequestsSigned,
};
}
if (!string.IsNullOrWhiteSpace(config.IdpX509PublicCert))
private Sustainsys.Saml2.WebSso.Saml2BindingType GetBindingType(Saml2BindingType bindingType)
{
var cert = CoreHelpers.Base64UrlDecode(config.IdpX509PublicCert);
idp.SigningKeys.AddConfiguredKey(new X509Certificate2(cert));
return bindingType switch
{
Saml2BindingType.HttpRedirect => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpRedirect,
Saml2BindingType.HttpPost => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,
_ => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,
};
}
idp.ArtifactResolutionServiceUrls.Clear();
// This must happen last since it calls Validate() internally.
idp.LoadMetadata = false;
var options = new Saml2Options
{
SPOptions = spOptions,
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,
CookieManager = new IdentityServer.DistributedCacheCookieManager(),
};
options.IdentityProviders.Add(idp);
return new DynamicAuthenticationScheme(name, name, typeof(Saml2Handler), options, SsoType.Saml2);
}
private NameIdFormat GetNameIdFormat(Saml2NameIdFormat format)
{
return format switch
{
Saml2NameIdFormat.Unspecified => NameIdFormat.Unspecified,
Saml2NameIdFormat.EmailAddress => NameIdFormat.EmailAddress,
Saml2NameIdFormat.X509SubjectName => NameIdFormat.X509SubjectName,
Saml2NameIdFormat.WindowsDomainQualifiedName => NameIdFormat.WindowsDomainQualifiedName,
Saml2NameIdFormat.KerberosPrincipalName => NameIdFormat.KerberosPrincipalName,
Saml2NameIdFormat.EntityIdentifier => NameIdFormat.EntityIdentifier,
Saml2NameIdFormat.Persistent => NameIdFormat.Persistent,
Saml2NameIdFormat.Transient => NameIdFormat.Transient,
_ => NameIdFormat.NotConfigured,
};
}
private SigningBehavior GetSigningBehavior(Saml2SigningBehavior behavior)
{
return behavior switch
{
Saml2SigningBehavior.IfIdpWantAuthnRequestsSigned => SigningBehavior.IfIdpWantAuthnRequestsSigned,
Saml2SigningBehavior.Always => SigningBehavior.Always,
Saml2SigningBehavior.Never => SigningBehavior.Never,
_ => SigningBehavior.IfIdpWantAuthnRequestsSigned,
};
}
private Sustainsys.Saml2.WebSso.Saml2BindingType GetBindingType(Saml2BindingType bindingType)
{
return bindingType switch
{
Saml2BindingType.HttpRedirect => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpRedirect,
Saml2BindingType.HttpPost => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,
_ => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,
};
}
}

View File

@@ -1,36 +1,37 @@
using System.Collections.Concurrent;
using Microsoft.Extensions.Options;
namespace Bit.Sso.Utilities;
public class ExtendedOptionsMonitorCache<TOptions> : IExtendedOptionsMonitorCache<TOptions> where TOptions : class
namespace Bit.Sso.Utilities
{
private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache =
new ConcurrentDictionary<string, Lazy<TOptions>>(StringComparer.Ordinal);
public void AddOrUpdate(string name, TOptions options)
public class ExtendedOptionsMonitorCache<TOptions> : IExtendedOptionsMonitorCache<TOptions> where TOptions : class
{
_cache.AddOrUpdate(name ?? Options.DefaultName, new Lazy<TOptions>(() => options),
(string s, Lazy<TOptions> lazy) => new Lazy<TOptions>(() => options));
}
private readonly ConcurrentDictionary<string, Lazy<TOptions>> _cache =
new ConcurrentDictionary<string, Lazy<TOptions>>(StringComparer.Ordinal);
public void Clear()
{
_cache.Clear();
}
public void AddOrUpdate(string name, TOptions options)
{
_cache.AddOrUpdate(name ?? Options.DefaultName, new Lazy<TOptions>(() => options),
(string s, Lazy<TOptions> lazy) => new Lazy<TOptions>(() => options));
}
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
return _cache.GetOrAdd(name ?? Options.DefaultName, new Lazy<TOptions>(createOptions)).Value;
}
public void Clear()
{
_cache.Clear();
}
public bool TryAdd(string name, TOptions options)
{
return _cache.TryAdd(name ?? Options.DefaultName, new Lazy<TOptions>(() => options));
}
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
return _cache.GetOrAdd(name ?? Options.DefaultName, new Lazy<TOptions>(createOptions)).Value;
}
public bool TryRemove(string name)
{
return _cache.TryRemove(name ?? Options.DefaultName, out _);
public bool TryAdd(string name, TOptions options)
{
return _cache.TryAdd(name ?? Options.DefaultName, new Lazy<TOptions>(() => options));
}
public bool TryRemove(string name)
{
return _cache.TryRemove(name ?? Options.DefaultName, out _);
}
}
}

View File

@@ -1,12 +1,13 @@
using Bit.Core.Enums;
using Microsoft.AspNetCore.Authentication;
namespace Bit.Sso.Utilities;
public interface IDynamicAuthenticationScheme
namespace Bit.Sso.Utilities
{
AuthenticationSchemeOptions Options { get; set; }
SsoType SsoType { get; set; }
public interface IDynamicAuthenticationScheme
{
AuthenticationSchemeOptions Options { get; set; }
SsoType SsoType { get; set; }
Task Validate();
Task Validate();
}
}

View File

@@ -1,8 +1,9 @@
using Microsoft.Extensions.Options;
namespace Bit.Sso.Utilities;
public interface IExtendedOptionsMonitorCache<TOptions> : IOptionsMonitorCache<TOptions> where TOptions : class
namespace Bit.Sso.Utilities
{
void AddOrUpdate(string name, TOptions options);
public interface IExtendedOptionsMonitorCache<TOptions> : IOptionsMonitorCache<TOptions> where TOptions : class
{
void AddOrUpdate(string name, TOptions options);
}
}

View File

@@ -1,62 +1,63 @@
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
namespace Bit.Sso.Utilities;
public static class OpenIdConnectOptionsExtensions
namespace Bit.Sso.Utilities
{
public static async Task<bool> CouldHandleAsync(this OpenIdConnectOptions options, string scheme, HttpContext context)
public static class OpenIdConnectOptionsExtensions
{
// Determine this is a valid request for our handler
if (options.CallbackPath != context.Request.Path &&
options.RemoteSignOutPath != context.Request.Path &&
options.SignedOutCallbackPath != context.Request.Path)
public static async Task<bool> CouldHandleAsync(this OpenIdConnectOptions options, string scheme, HttpContext context)
{
return false;
}
if (context.Request.Query["scheme"].FirstOrDefault() == scheme)
{
return true;
}
try
{
// Parse out the message
OpenIdConnectMessage message = null;
if (string.Equals(context.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
// Determine this is a valid request for our handler
if (options.CallbackPath != context.Request.Path &&
options.RemoteSignOutPath != context.Request.Path &&
options.SignedOutCallbackPath != context.Request.Path)
{
message = new OpenIdConnectMessage(context.Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
}
else if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(context.Request.ContentType) &&
context.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) &&
context.Request.Body.CanRead)
{
var form = await context.Request.ReadFormAsync();
message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
}
var state = message?.State;
if (string.IsNullOrWhiteSpace(state))
{
// State is required, it will fail later on for this reason.
return false;
}
// Handle State if we've gotten that back
var decodedState = options.StateDataFormat.Unprotect(state);
if (decodedState != null && decodedState.Items.ContainsKey("scheme"))
if (context.Request.Query["scheme"].FirstOrDefault() == scheme)
{
return decodedState.Items["scheme"] == scheme;
return true;
}
}
catch
{
try
{
// Parse out the message
OpenIdConnectMessage message = null;
if (string.Equals(context.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
message = new OpenIdConnectMessage(context.Request.Query.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
}
else if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrEmpty(context.Request.ContentType) &&
context.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) &&
context.Request.Body.CanRead)
{
var form = await context.Request.ReadFormAsync();
message = new OpenIdConnectMessage(form.Select(pair => new KeyValuePair<string, string[]>(pair.Key, pair.Value)));
}
var state = message?.State;
if (string.IsNullOrWhiteSpace(state))
{
// State is required, it will fail later on for this reason.
return false;
}
// Handle State if we've gotten that back
var decodedState = options.StateDataFormat.Unprotect(state);
if (decodedState != null && decodedState.Items.ContainsKey("scheme"))
{
return decodedState.Items["scheme"] == scheme;
}
}
catch
{
return false;
}
// This is likely not an appropriate handler
return false;
}
// This is likely not an appropriate handler
return false;
}
}

View File

@@ -1,63 +1,64 @@
namespace Bit.Sso.Utilities;
/// <summary>
/// OpenID Connect Clients use scope values as defined in 3.3 of OAuth 2.0
/// [RFC6749]. These values represent the standard scope values supported
/// by OAuth 2.0 and therefore OIDC.
/// </summary>
/// <remarks>
/// See: https://openid.net/specs/openid-connect-basic-1_0.html#Scopes
/// </remarks>
public static class OpenIdConnectScopes
namespace Bit.Sso.Utilities
{
/// <summary>
/// REQUIRED. Informs the Authorization Server that the Client is making
/// an OpenID Connect request. If the openid scope value is not present,
/// the behavior is entirely unspecified.
/// </summary>
public const string OpenId = "openid";
/// <summary>
/// OPTIONAL. This scope value requests access to the End-User's default
/// profile Claims, which are: name, family_name, given_name,
/// middle_name, nickname, preferred_username, profile, picture,
/// website, gender, birthdate, zoneinfo, locale, and updated_at.
/// </summary>
public const string Profile = "profile";
/// <summary>
/// OPTIONAL. This scope value requests access to the email and
/// email_verified Claims.
/// </summary>
public const string Email = "email";
/// <summary>
/// OPTIONAL. This scope value requests access to the address Claim.
/// </summary>
public const string Address = "address";
/// <summary>
/// OPTIONAL. This scope value requests access to the phone_number and
/// phone_number_verified Claims.
/// </summary>
public const string Phone = "phone";
/// <summary>
/// OPTIONAL. This scope value requests that an OAuth 2.0 Refresh Token
/// be issued that can be used to obtain an Access Token that grants
/// access to the End-User's UserInfo Endpoint even when the End-User is
/// not present (not logged in).
/// </summary>
public const string OfflineAccess = "offline_access";
/// <summary>
/// OPTIONAL. Authentication Context Class Reference. String specifying
/// an Authentication Context Class Reference value that identifies the
/// Authentication Context Class that the authentication performed
/// satisfied.
/// OpenID Connect Clients use scope values as defined in 3.3 of OAuth 2.0
/// [RFC6749]. These values represent the standard scope values supported
/// by OAuth 2.0 and therefore OIDC.
/// </summary>
/// <remarks>
/// See: https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2
/// See: https://openid.net/specs/openid-connect-basic-1_0.html#Scopes
/// </remarks>
public const string Acr = "acr";
public static class OpenIdConnectScopes
{
/// <summary>
/// REQUIRED. Informs the Authorization Server that the Client is making
/// an OpenID Connect request. If the openid scope value is not present,
/// the behavior is entirely unspecified.
/// </summary>
public const string OpenId = "openid";
/// <summary>
/// OPTIONAL. This scope value requests access to the End-User's default
/// profile Claims, which are: name, family_name, given_name,
/// middle_name, nickname, preferred_username, profile, picture,
/// website, gender, birthdate, zoneinfo, locale, and updated_at.
/// </summary>
public const string Profile = "profile";
/// <summary>
/// OPTIONAL. This scope value requests access to the email and
/// email_verified Claims.
/// </summary>
public const string Email = "email";
/// <summary>
/// OPTIONAL. This scope value requests access to the address Claim.
/// </summary>
public const string Address = "address";
/// <summary>
/// OPTIONAL. This scope value requests access to the phone_number and
/// phone_number_verified Claims.
/// </summary>
public const string Phone = "phone";
/// <summary>
/// OPTIONAL. This scope value requests that an OAuth 2.0 Refresh Token
/// be issued that can be used to obtain an Access Token that grants
/// access to the End-User's UserInfo Endpoint even when the End-User is
/// not present (not logged in).
/// </summary>
public const string OfflineAccess = "offline_access";
/// <summary>
/// OPTIONAL. Authentication Context Class Reference. String specifying
/// an Authentication Context Class Reference value that identifies the
/// Authentication Context Class that the authentication performed
/// satisfied.
/// </summary>
/// <remarks>
/// See: https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.2
/// </remarks>
public const string Acr = "acr";
}
}

View File

@@ -4,101 +4,102 @@ using System.Xml;
using Sustainsys.Saml2;
using Sustainsys.Saml2.AspNetCore2;
namespace Bit.Sso.Utilities;
public static class Saml2OptionsExtensions
namespace Bit.Sso.Utilities
{
public static async Task<bool> CouldHandleAsync(this Saml2Options options, string scheme, HttpContext context)
public static class Saml2OptionsExtensions
{
// Determine this is a valid request for our handler
if (!context.Request.Path.StartsWithSegments(options.SPOptions.ModulePath, StringComparison.Ordinal))
public static async Task<bool> CouldHandleAsync(this Saml2Options options, string scheme, HttpContext context)
{
return false;
}
// Determine this is a valid request for our handler
if (!context.Request.Path.StartsWithSegments(options.SPOptions.ModulePath, StringComparison.Ordinal))
{
return false;
}
var idp = options.IdentityProviders.IsEmpty ? null : options.IdentityProviders.Default;
if (idp == null)
{
return false;
}
var idp = options.IdentityProviders.IsEmpty ? null : options.IdentityProviders.Default;
if (idp == null)
{
return false;
}
if (context.Request.Query["scheme"].FirstOrDefault() == scheme)
{
return true;
}
// We need to pull out and parse the response or request SAML envelope
XmlElement envelope = null;
try
{
if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) &&
context.Request.HasFormContentType)
{
string encodedMessage;
if (context.Request.Form.TryGetValue("SAMLResponse", out var response))
{
encodedMessage = response.FirstOrDefault();
}
else
{
encodedMessage = context.Request.Form["SAMLRequest"];
}
if (string.IsNullOrWhiteSpace(encodedMessage))
{
return false;
}
envelope = XmlHelpers.XmlDocumentFromString(
Encoding.UTF8.GetString(Convert.FromBase64String(encodedMessage)))?.DocumentElement;
}
else if (string.Equals(context.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
var encodedPayload = context.Request.Query["SAMLRequest"].FirstOrDefault() ??
context.Request.Query["SAMLResponse"].FirstOrDefault();
try
{
var payload = Convert.FromBase64String(encodedPayload);
using var compressed = new MemoryStream(payload);
using var decompressedStream = new DeflateStream(compressed, CompressionMode.Decompress, true);
using var deCompressed = new MemoryStream();
await decompressedStream.CopyToAsync(deCompressed);
envelope = XmlHelpers.XmlDocumentFromString(
Encoding.UTF8.GetString(deCompressed.GetBuffer(), 0, (int)deCompressed.Length))?.DocumentElement;
}
catch (FormatException ex)
{
throw new FormatException($"\'{encodedPayload}\' is not a valid Base64 encoded string: {ex.Message}", ex);
}
}
}
catch
{
return false;
}
if (envelope == null)
{
return false;
}
// Double check the entity Ids
var entityId = envelope["Issuer", Saml2Namespaces.Saml2Name]?.InnerText.Trim();
if (!string.Equals(entityId, idp.EntityId.Id, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
if (options.SPOptions.WantAssertionsSigned)
{
var assertion = envelope["Assertion", Saml2Namespaces.Saml2Name];
var isAssertionSigned = assertion != null && XmlHelpers.IsSignedByAny(assertion, idp.SigningKeys,
options.SPOptions.ValidateCertificates, options.SPOptions.MinIncomingSigningAlgorithm);
if (!isAssertionSigned)
{
throw new Exception("Cannot verify SAML assertion signature.");
}
}
if (context.Request.Query["scheme"].FirstOrDefault() == scheme)
{
return true;
}
// We need to pull out and parse the response or request SAML envelope
XmlElement envelope = null;
try
{
if (string.Equals(context.Request.Method, "POST", StringComparison.OrdinalIgnoreCase) &&
context.Request.HasFormContentType)
{
string encodedMessage;
if (context.Request.Form.TryGetValue("SAMLResponse", out var response))
{
encodedMessage = response.FirstOrDefault();
}
else
{
encodedMessage = context.Request.Form["SAMLRequest"];
}
if (string.IsNullOrWhiteSpace(encodedMessage))
{
return false;
}
envelope = XmlHelpers.XmlDocumentFromString(
Encoding.UTF8.GetString(Convert.FromBase64String(encodedMessage)))?.DocumentElement;
}
else if (string.Equals(context.Request.Method, "GET", StringComparison.OrdinalIgnoreCase))
{
var encodedPayload = context.Request.Query["SAMLRequest"].FirstOrDefault() ??
context.Request.Query["SAMLResponse"].FirstOrDefault();
try
{
var payload = Convert.FromBase64String(encodedPayload);
using var compressed = new MemoryStream(payload);
using var decompressedStream = new DeflateStream(compressed, CompressionMode.Decompress, true);
using var deCompressed = new MemoryStream();
await decompressedStream.CopyToAsync(deCompressed);
envelope = XmlHelpers.XmlDocumentFromString(
Encoding.UTF8.GetString(deCompressed.GetBuffer(), 0, (int)deCompressed.Length))?.DocumentElement;
}
catch (FormatException ex)
{
throw new FormatException($"\'{encodedPayload}\' is not a valid Base64 encoded string: {ex.Message}", ex);
}
}
}
catch
{
return false;
}
if (envelope == null)
{
return false;
}
// Double check the entity Ids
var entityId = envelope["Issuer", Saml2Namespaces.Saml2Name]?.InnerText.Trim();
if (!string.Equals(entityId, idp.EntityId.Id, StringComparison.InvariantCultureIgnoreCase))
{
return false;
}
if (options.SPOptions.WantAssertionsSigned)
{
var assertion = envelope["Assertion", Saml2Namespaces.Saml2Name];
var isAssertionSigned = assertion != null && XmlHelpers.IsSignedByAny(assertion, idp.SigningKeys,
options.SPOptions.ValidateCertificates, options.SPOptions.MinIncomingSigningAlgorithm);
if (!isAssertionSigned)
{
throw new Exception("Cannot verify SAML assertion signature.");
}
}
return true;
}
}

View File

@@ -1,11 +1,12 @@
namespace Bit.Sso.Utilities;
public static class SamlClaimTypes
namespace Bit.Sso.Utilities
{
public const string Email = "urn:oid:0.9.2342.19200300.100.1.3";
public const string GivenName = "urn:oid:2.5.4.42";
public const string Surname = "urn:oid:2.5.4.4";
public const string DisplayName = "urn:oid:2.16.840.1.113730.3.1.241";
public const string CommonName = "urn:oid:2.5.4.3";
public const string UserId = "urn:oid:0.9.2342.19200300.100.1.1";
public static class SamlClaimTypes
{
public const string Email = "urn:oid:0.9.2342.19200300.100.1.3";
public const string GivenName = "urn:oid:2.5.4.42";
public const string Surname = "urn:oid:2.5.4.4";
public const string DisplayName = "urn:oid:2.16.840.1.113730.3.1.241";
public const string CommonName = "urn:oid:2.5.4.3";
public const string UserId = "urn:oid:0.9.2342.19200300.100.1.1";
}
}

View File

@@ -1,17 +1,18 @@
namespace Bit.Sso.Utilities;
public static class SamlNameIdFormats
namespace Bit.Sso.Utilities
{
// Common
public const string Unspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
public const string Email = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
public const string Persistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent";
public const string Transient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
// Not-so-common
public const string Upn = "http://schemas.xmlsoap.org/claims/UPN";
public const string CommonName = "http://schemas.xmlsoap.org/claims/CommonName";
public const string X509SubjectName = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName";
public const string WindowsQualifiedDomainName = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName";
public const string KerberosPrincipalName = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos";
public const string EntityIdentifier = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity";
public static class SamlNameIdFormats
{
// Common
public const string Unspecified = "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified";
public const string Email = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress";
public const string Persistent = "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent";
public const string Transient = "urn:oasis:names:tc:SAML:2.0:nameid-format:transient";
// Not-so-common
public const string Upn = "http://schemas.xmlsoap.org/claims/UPN";
public const string CommonName = "http://schemas.xmlsoap.org/claims/CommonName";
public const string X509SubjectName = "urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName";
public const string WindowsQualifiedDomainName = "urn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName";
public const string KerberosPrincipalName = "urn:oasis:names:tc:SAML:2.0:nameid-format:kerberos";
public const string EntityIdentifier = "urn:oasis:names:tc:SAML:2.0:nameid-format:entity";
}
}

View File

@@ -1,6 +1,7 @@
namespace Bit.Sso.Utilities;
public static class SamlPropertyKeys
namespace Bit.Sso.Utilities
{
public const string ClaimFormat = "http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format";
public static class SamlPropertyKeys
{
public const string ClaimFormat = "http://schemas.xmlsoap.org/ws/2005/05/identity/claimproperties/format";
}
}

View File

@@ -9,69 +9,70 @@ using IdentityServer4.ResponseHandling;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Sustainsys.Saml2.AspNetCore2;
namespace Bit.Sso.Utilities;
public static class ServiceCollectionExtensions
namespace Bit.Sso.Utilities
{
public static IServiceCollection AddSsoServices(this IServiceCollection services,
GlobalSettings globalSettings)
public static class ServiceCollectionExtensions
{
// SAML SP Configuration
var samlEnvironment = new SamlEnvironment
public static IServiceCollection AddSsoServices(this IServiceCollection services,
GlobalSettings globalSettings)
{
SpSigningCertificate = CoreHelpers.GetIdentityServerCertificate(globalSettings),
};
services.AddSingleton(s => samlEnvironment);
services.AddSingleton<Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider,
DynamicAuthenticationSchemeProvider>();
// Oidc
services.AddSingleton<Microsoft.Extensions.Options.IPostConfigureOptions<OpenIdConnectOptions>,
OpenIdConnectPostConfigureOptions>();
services.AddSingleton<Microsoft.Extensions.Options.IOptionsMonitorCache<OpenIdConnectOptions>,
ExtendedOptionsMonitorCache<OpenIdConnectOptions>>();
// Saml2
services.AddSingleton<Microsoft.Extensions.Options.IPostConfigureOptions<Saml2Options>,
PostConfigureSaml2Options>();
services.AddSingleton<Microsoft.Extensions.Options.IOptionsMonitorCache<Saml2Options>,
ExtendedOptionsMonitorCache<Saml2Options>>();
return services;
}
public static IIdentityServerBuilder AddSsoIdentityServerServices(this IServiceCollection services,
IWebHostEnvironment env, GlobalSettings globalSettings)
{
services.AddTransient<IDiscoveryResponseGenerator, DiscoveryResponseGenerator>();
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalSso);
var identityServerBuilder = services
.AddIdentityServer(options =>
// SAML SP Configuration
var samlEnvironment = new SamlEnvironment
{
options.IssuerUri = $"{issuerUri.Scheme}://{issuerUri.Host}";
if (env.IsDevelopment())
SpSigningCertificate = CoreHelpers.GetIdentityServerCertificate(globalSettings),
};
services.AddSingleton(s => samlEnvironment);
services.AddSingleton<Microsoft.AspNetCore.Authentication.IAuthenticationSchemeProvider,
DynamicAuthenticationSchemeProvider>();
// Oidc
services.AddSingleton<Microsoft.Extensions.Options.IPostConfigureOptions<OpenIdConnectOptions>,
OpenIdConnectPostConfigureOptions>();
services.AddSingleton<Microsoft.Extensions.Options.IOptionsMonitorCache<OpenIdConnectOptions>,
ExtendedOptionsMonitorCache<OpenIdConnectOptions>>();
// Saml2
services.AddSingleton<Microsoft.Extensions.Options.IPostConfigureOptions<Saml2Options>,
PostConfigureSaml2Options>();
services.AddSingleton<Microsoft.Extensions.Options.IOptionsMonitorCache<Saml2Options>,
ExtendedOptionsMonitorCache<Saml2Options>>();
return services;
}
public static IIdentityServerBuilder AddSsoIdentityServerServices(this IServiceCollection services,
IWebHostEnvironment env, GlobalSettings globalSettings)
{
services.AddTransient<IDiscoveryResponseGenerator, DiscoveryResponseGenerator>();
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalSso);
var identityServerBuilder = services
.AddIdentityServer(options =>
{
options.Authentication.CookieSameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;
}
else
options.IssuerUri = $"{issuerUri.Scheme}://{issuerUri.Host}";
if (env.IsDevelopment())
{
options.Authentication.CookieSameSiteMode = Microsoft.AspNetCore.Http.SameSiteMode.Unspecified;
}
else
{
options.UserInteraction.ErrorUrl = "/Error";
options.UserInteraction.ErrorIdParameter = "errorId";
}
options.InputLengthRestrictions.UserName = 256;
})
.AddInMemoryCaching()
.AddInMemoryClients(new List<Client>
{
options.UserInteraction.ErrorUrl = "/Error";
options.UserInteraction.ErrorIdParameter = "errorId";
}
options.InputLengthRestrictions.UserName = 256;
})
.AddInMemoryCaching()
.AddInMemoryClients(new List<Client>
{
new OidcIdentityClient(globalSettings)
})
.AddInMemoryIdentityResources(new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
})
.AddIdentityServerCertificate(env, globalSettings);
new OidcIdentityClient(globalSettings)
})
.AddInMemoryIdentityResources(new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
})
.AddIdentityServerCertificate(env, globalSettings);
return identityServerBuilder;
return identityServerBuilder;
}
}
}

View File

@@ -3,82 +3,83 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Sustainsys.Saml2.AspNetCore2;
namespace Bit.Sso.Utilities;
public class SsoAuthenticationMiddleware
namespace Bit.Sso.Utilities
{
private readonly RequestDelegate _next;
public SsoAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
public class SsoAuthenticationMiddleware
{
_next = next ?? throw new ArgumentNullException(nameof(next));
Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
}
private readonly RequestDelegate _next;
public IAuthenticationSchemeProvider Schemes { get; set; }
public async Task Invoke(HttpContext context)
{
if ((context.Request.Method == "GET" && context.Request.Query.ContainsKey("SAMLart"))
|| (context.Request.Method == "POST" && context.Request.Form.ContainsKey("SAMLart")))
public SsoAuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
throw new Exception("SAMLart parameter detected. SAML Artifact binding is not allowed.");
_next = next ?? throw new ArgumentNullException(nameof(next));
Schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
}
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
public IAuthenticationSchemeProvider Schemes { get; set; }
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
public async Task Invoke(HttpContext context)
{
// Determine if scheme is appropriate for the current context FIRST
if (scheme is IDynamicAuthenticationScheme dynamicScheme)
if ((context.Request.Method == "GET" && context.Request.Query.ContainsKey("SAMLart"))
|| (context.Request.Method == "POST" && context.Request.Form.ContainsKey("SAMLart")))
{
switch (dynamicScheme.SsoType)
throw new Exception("SAMLart parameter detected. SAML Artifact binding is not allowed.");
}
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
// Give any IAuthenticationRequestHandler schemes a chance to handle the request
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
// Determine if scheme is appropriate for the current context FIRST
if (scheme is IDynamicAuthenticationScheme dynamicScheme)
{
case SsoType.OpenIdConnect:
default:
if (dynamicScheme.Options is OpenIdConnectOptions oidcOptions &&
!await oidcOptions.CouldHandleAsync(scheme.Name, context))
{
// It's OIDC and Dynamic, but not a good fit
continue;
}
break;
case SsoType.Saml2:
if (dynamicScheme.Options is Saml2Options samlOptions &&
!await samlOptions.CouldHandleAsync(scheme.Name, context))
{
// It's SAML and Dynamic, but not a good fit
continue;
}
break;
switch (dynamicScheme.SsoType)
{
case SsoType.OpenIdConnect:
default:
if (dynamicScheme.Options is OpenIdConnectOptions oidcOptions &&
!await oidcOptions.CouldHandleAsync(scheme.Name, context))
{
// It's OIDC and Dynamic, but not a good fit
continue;
}
break;
case SsoType.Saml2:
if (dynamicScheme.Options is Saml2Options samlOptions &&
!await samlOptions.CouldHandleAsync(scheme.Name, context))
{
// It's SAML and Dynamic, but not a good fit
continue;
}
break;
}
}
// This far it's not dynamic OR it is but "could" be handled
if (await handlers.GetHandlerAsync(context, scheme.Name) is IAuthenticationRequestHandler handler &&
await handler.HandleRequestAsync())
{
return;
}
}
// This far it's not dynamic OR it is but "could" be handled
if (await handlers.GetHandlerAsync(context, scheme.Name) is IAuthenticationRequestHandler handler &&
await handler.HandleRequestAsync())
// Fallback to the default scheme from the provider
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
return;
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null)
{
context.User = result.Principal;
}
}
}
// Fallback to the default scheme from the provider
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null)
{
context.User = result.Principal;
}
await _next(context);
}
await _next(context);
}
}