Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
using Microsoft.IdentityModel.Tokens;
using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.Configuration;
using Sustainsys.Saml2.Metadata;
using Sustainsys.Saml2.Saml2P;

namespace Bit.Core.Business.Sso;
Expand Down Expand Up @@ -421,6 +422,8 @@ private DynamicAuthenticationScheme GetSaml2AuthenticationScheme(string name, Ss
};
options.IdentityProviders.Add(idp);

options.Notifications.MetadataCreated += OnMetadataCreated;

return new DynamicAuthenticationScheme(name, name, typeof(Saml2Handler), options, SsoType.Saml2);
}

Expand Down Expand Up @@ -460,4 +463,26 @@ private Sustainsys.Saml2.WebSso.Saml2BindingType GetBindingType(Saml2BindingType
_ => Sustainsys.Saml2.WebSso.Saml2BindingType.HttpPost,
};
}

/// <summary>
/// Adds the accepted key-transport algorithms to every encryption-capable <c>KeyDescriptor</c>
/// in the published Service Provider metadata when it is created.
/// </summary>
public static void OnMetadataCreated(
EntityDescriptor entityDescriptor,
Sustainsys.Saml2.WebSso.Saml2Urls _)
{
var keyDescriptors = entityDescriptor.RoleDescriptors
.OfType<SpSsoDescriptor>()
.SelectMany(d => d.Keys)
.Where(k => k.Use != KeyType.Signing);

foreach (var keyDescriptor in keyDescriptors)
{
foreach (var algorithm in SamlEncryptionAlgorithms.Accepted)
{
keyDescriptor.EncryptionMethods.Add(new EncryptionMethod { Algorithm = new Uri(algorithm) });
}
}
}
}
16 changes: 16 additions & 0 deletions bitwarden_license/src/Sso/Utilities/SamlEncryptionAlgorithms.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
ο»Ώnamespace Bit.Sso.Utilities;

public static class SamlEncryptionAlgorithms
{
public const string RsaOaepMgf1p = "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p";

public const string RsaOaep = "http://www.w3.org/2009/xmlenc11#rsa-oaep";

// Key-transport algorithms advertised in Service Provider (SP) metadata.
// Order is important! rsa-oaep-mgf1p must come first.
// IdPs will generally choose the first advertised method found.
// rsa-oaep is more generic, and requires an IdP to also transmit a
// <ds:DigestMethod> Digest Method. Without a Digest Method, rsa-oaep
// will throw on decryption.
public static readonly string[] Accepted = [RsaOaepMgf1p, RsaOaep];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
ο»Ώusing System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Business.Sso;
using Bit.Core.Utilities;
using Bit.Sso.Models;
using Bit.Sso.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Extensions.Options;
using NSubstitute;
using Sustainsys.Saml2.AspNetCore2;
using Sustainsys.Saml2.Metadata;

namespace Bit.SSO.Test.Utilities;

public class DynamicAuthenticationSchemeProviderTests
{
[Fact]
public void OnMetadataCreated_NonSigningKeyExists_AddsAcceptedAlgorithmsInOrder()
{
var keyDescriptor = new KeyDescriptor { Use = KeyType.Unspecified };
var entityDescriptor = BuildEntityDescriptor(keyDescriptor);

DynamicAuthenticationSchemeProvider.OnMetadataCreated(entityDescriptor, null!);

Assert.Equal(
SamlEncryptionAlgorithms.Accepted,
keyDescriptor.EncryptionMethods.Select(m => m.Algorithm.ToString()));
}

[Fact]
public void OnMetadataCreated_NonSigningKeyExists_AddsAlgorithmsInDocumentedLiteralOrder()
{
var keyDescriptor = new KeyDescriptor { Use = KeyType.Unspecified };
var entityDescriptor = BuildEntityDescriptor(keyDescriptor);

DynamicAuthenticationSchemeProvider.OnMetadataCreated(entityDescriptor, null!);

// rsa-oaep-mgf1p must come first; IdPs generally choose the first advertised method.
Assert.Equal(
new[]
{
"http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p",
"http://www.w3.org/2009/xmlenc11#rsa-oaep",
},
keyDescriptor.EncryptionMethods.Select(m => m.Algorithm.ToString()));
}

[Fact]
public void OnMetadataCreated_OnlySigningKeyExists_AddsNoEncryptionMethods()
{
var signingKey = new KeyDescriptor { Use = KeyType.Signing };
var entityDescriptor = BuildEntityDescriptor(signingKey);

DynamicAuthenticationSchemeProvider.OnMetadataCreated(entityDescriptor, null!);

Assert.Empty(signingKey.EncryptionMethods);
}

[Fact]
public void OnMetadataCreated_NoSpSsoDescriptor_DoesNotThrow()
{
var entityDescriptor = new EntityDescriptor();

var exception = Record.Exception(() =>
DynamicAuthenticationSchemeProvider.OnMetadataCreated(entityDescriptor, null!));

Assert.Null(exception);
}

[Fact]
public void OnMetadataCreated_MultipleNonSigningKeys_AllKeysGetMethods()
{
var firstKey = new KeyDescriptor { Use = KeyType.Encryption };
var secondKey = new KeyDescriptor { Use = KeyType.Unspecified };
var spSsoDescriptor = new SpSsoDescriptor();
spSsoDescriptor.Keys.Add(firstKey);
spSsoDescriptor.Keys.Add(secondKey);
var entityDescriptor = new EntityDescriptor();
entityDescriptor.RoleDescriptors.Add(spSsoDescriptor);

DynamicAuthenticationSchemeProvider.OnMetadataCreated(entityDescriptor, null!);

Assert.Equal(
SamlEncryptionAlgorithms.Accepted,
firstKey.EncryptionMethods.Select(m => m.Algorithm.ToString()));
Assert.Equal(
SamlEncryptionAlgorithms.Accepted,
secondKey.EncryptionMethods.Select(m => m.Algorithm.ToString()));
}

[Theory, BitAutoData]
public async Task GetSchemeAsync_Saml2Config_WiresUpMetadataCreatedNotification(
Guid organizationId,
SutProvider<DynamicAuthenticationSchemeProvider> sutProvider)
{
sutProvider.SetDependency<IOptions<AuthenticationOptions>>(
Options.Create(new AuthenticationOptions()));
sutProvider.SetDependency<IOptionsMonitorCache<Saml2Options>>(
Substitute.For<IExtendedOptionsMonitorCache<Saml2Options>>());
sutProvider.SetDependency<IOptionsMonitorCache<OpenIdConnectOptions>>(
Substitute.For<IExtendedOptionsMonitorCache<OpenIdConnectOptions>>());
sutProvider.SetDependency(new SamlEnvironment());
sutProvider.Create();

using var idpSigningKey = RSA.Create(2048);
var now = DateTimeOffset.UtcNow;
using var idpSigningCertificate = new CertificateRequest(
"CN=Test IdP signing certificate", idpSigningKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
.CreateSelfSigned(now, now.AddDays(365));

var ssoConfig = new SsoConfig { OrganizationId = organizationId, Enabled = true };
ssoConfig.SetData(new SsoConfigurationData
{
ConfigType = SsoType.Saml2,
IdpEntityId = "https://idp.example.com",
IdpSingleSignOnServiceUrl = "https://idp.example.com/sso",
IdpX509PublicCert = CoreHelpers.Base64UrlEncode(idpSigningCertificate.RawData),
});
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(organizationId)
.Returns(ssoConfig);

var scheme = await sutProvider.Sut.GetSchemeAsync(organizationId.ToString());

var saml2Options = Assert.IsType<Saml2Options>(((DynamicAuthenticationScheme)scheme).Options);
Assert.Contains(
saml2Options.Notifications.MetadataCreated.GetInvocationList(),
d => d.Method.Name == nameof(DynamicAuthenticationSchemeProvider.OnMetadataCreated));
}

private static EntityDescriptor BuildEntityDescriptor(KeyDescriptor keyDescriptor)
{
var spSsoDescriptor = new SpSsoDescriptor();
spSsoDescriptor.Keys.Add(keyDescriptor);
var entityDescriptor = new EntityDescriptor();
entityDescriptor.RoleDescriptors.Add(spSsoDescriptor);
return entityDescriptor;
}
}
Loading