-
+
@@ -245,7 +275,7 @@
@@ -278,7 +308,7 @@
-
+
diff --git a/bitwarden_license/src/Sso/Controllers/AccountController.cs b/bitwarden_license/src/Sso/Controllers/AccountController.cs
index a627773f4..a2cfeefee 100644
--- a/bitwarden_license/src/Sso/Controllers/AccountController.cs
+++ b/bitwarden_license/src/Sso/Controllers/AccountController.cs
@@ -23,6 +23,8 @@ using System.Threading.Tasks;
using Bit.Core.Models;
using Bit.Core.Models.Api;
using Bit.Core.Utilities;
+using System.Text.Json;
+using Bit.Core.Models.Data;
namespace Bit.Sso.Controllers
{
@@ -204,7 +206,7 @@ namespace Bit.Sso.Controllers
{
// Read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(
- Core.AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
+ AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
if (result?.Succeeded != true)
{
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
@@ -215,7 +217,7 @@ namespace Bit.Sso.Controllers
_logger.LogDebug("External claims: {@claims}", externalClaims);
// Lookup our user and external provider info
- var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
+ var (user, provider, providerUserId, claims, ssoConfigData) = await FindUserFromExternalProviderAsync(result);
if (user == null)
{
// This might be where you might initiate a custom workflow for user registration
@@ -223,7 +225,7 @@ namespace Bit.Sso.Controllers
// simply auto-provisions new external user
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
result.Properties.Items["user_identifier"] : null;
- user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier);
+ user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData);
}
if (user != null)
@@ -305,9 +307,23 @@ namespace Bit.Sso.Controllers
}
}
- private async Task<(User user, string provider, string providerUserId, IEnumerable
claims)>
+ private async Task<(User user, string provider, string providerUserId, IEnumerable claims, SsoConfigurationData config)>
FindUserFromExternalProviderAsync(AuthenticateResult result)
{
+ var provider = result.Properties.Items["scheme"];
+ var orgId = new Guid(provider);
+ var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
+ if (ssoConfig == null || !ssoConfig.Enabled)
+ {
+ throw new Exception(_i18nService.T("OrganizationOrSsoConfigNotFound"));
+ }
+
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+ var ssoConfigData = JsonSerializer.Deserialize(ssoConfig.Data, options);
+
var externalUser = result.Principal;
// Ensure the NameIdentifier used is not a transient name ID, if so, we need a different attribute
@@ -320,7 +336,9 @@ namespace Bit.Sso.Controllers
// Try to determine the unique id of the external user (issued by the provider)
// the most common claim type for that are the sub claim and the NameIdentifier
// depending on the external provider, some other claim type might be used
- var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
+ var customUserIdClaimTypes = ssoConfigData.GetAdditionalUserIdClaimTypes();
+ var userIdClaim = externalUser.FindFirst(c => customUserIdClaimTypes.Contains(c.Type)) ??
+ externalUser.FindFirst(JwtClaimTypes.Subject) ??
externalUser.FindFirst(nameIdIsNotTransient) ??
// Some SAML providers may use the `uid` attribute for this
// where a transient NameID has been sent in the subject
@@ -333,26 +351,19 @@ namespace Bit.Sso.Controllers
var claims = externalUser.Claims.ToList();
claims.Remove(userIdClaim);
- var provider = result.Properties.Items["scheme"];
+ // find external user
var providerUserId = userIdClaim.Value;
- // find external user
- var orgId = new Guid(provider);
- var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
- if (ssoConfig == null || !ssoConfig.Enabled)
- {
- throw new Exception(_i18nService.T("OrganizationOrSsoConfigNotFound"));
- }
var user = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);
- return (user, provider, providerUserId, claims);
+ return (user, provider, providerUserId, claims, ssoConfigData);
}
private async Task AutoProvisionUserAsync(string provider, string providerUserId,
- IEnumerable claims, string userIdentifier)
+ IEnumerable claims, string userIdentifier, SsoConfigurationData config)
{
- var name = GetName(claims);
- var email = GetEmailAddress(claims);
+ var name = GetName(claims, config.GetAdditionalNameClaimTypes());
+ var email = GetEmailAddress(claims, config.GetAdditionalEmailClaimTypes());
if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains("@"))
{
email = providerUserId;
@@ -498,12 +509,13 @@ namespace Bit.Sso.Controllers
return user;
}
- private string GetEmailAddress(IEnumerable claims)
+ private string GetEmailAddress(IEnumerable claims, IEnumerable additionalClaimTypes)
{
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
- var email = filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
- SamlClaimTypes.Email, "mail", "emailaddress");
+ var email = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
+ filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
+ SamlClaimTypes.Email, "mail", "emailaddress");
if (!string.IsNullOrWhiteSpace(email))
{
return email;
@@ -519,12 +531,13 @@ namespace Bit.Sso.Controllers
return null;
}
- private string GetName(IEnumerable claims)
+ private string GetName(IEnumerable claims, IEnumerable additionalClaimTypes)
{
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value));
- var name = filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
- SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
+ var name = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
+ filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
+ SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
if (!string.IsNullOrWhiteSpace(name))
{
return name;
diff --git a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs
index ded86fde6..22d69b8ac 100644
--- a/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs
+++ b/bitwarden_license/src/Sso/Utilities/DynamicAuthenticationSchemeProvider.cs
@@ -10,6 +10,7 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Sso;
+using Bit.Core.Utilities;
using Bit.Sso.Models;
using Bit.Sso.Utilities;
using IdentityModel;
@@ -324,21 +325,30 @@ namespace Bit.Core.Business.Sso
AuthenticationMethod = config.RedirectBehavior,
GetClaimsFromUserInfoEndpoint = config.GetClaimsFromUserInfoEndpoint,
};
- if (!oidcOptions.Scope.Contains(OpenIdConnectScopes.OpenId))
+ oidcOptions.Scope
+ .AddIfNotExists(OpenIdConnectScopes.OpenId)
+ .AddIfNotExists(OpenIdConnectScopes.Email)
+ .AddIfNotExists(OpenIdConnectScopes.Profile);
+ foreach (var scope in config.GetAdditionalScopes())
{
- oidcOptions.Scope.Add(OpenIdConnectScopes.OpenId);
- }
- if (!oidcOptions.Scope.Contains(OpenIdConnectScopes.Email))
- {
- oidcOptions.Scope.Add(OpenIdConnectScopes.Email);
- }
- if (!oidcOptions.Scope.Contains(OpenIdConnectScopes.Profile))
- {
- oidcOptions.Scope.Add(OpenIdConnectScopes.Profile);
+ oidcOptions.Scope.AddIfNotExists(scope);
}
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
+ {
+ OnRedirectToIdentityProvider = ctx =>
+ {
+ ctx.ProtocolMessage.AcrValues = config.AcrValues;
+ return Task.CompletedTask;
+ }
+ };
+ }
+
return new DynamicAuthenticationScheme(name, name, typeof(OpenIdConnectHandler),
oidcOptions, SsoType.OpenIdConnect);
}
diff --git a/src/Core/Models/Data/SsoConfigurationData.cs b/src/Core/Models/Data/SsoConfigurationData.cs
index e3d6675d5..d7c357a49 100644
--- a/src/Core/Models/Data/SsoConfigurationData.cs
+++ b/src/Core/Models/Data/SsoConfigurationData.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using Bit.Core.Enums;
using Bit.Core.Sso;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
@@ -20,6 +22,11 @@ namespace Bit.Core.Models.Data
public string MetadataAddress { get; set; }
public OpenIdConnectRedirectBehavior RedirectBehavior { get; set; } = OpenIdConnectRedirectBehavior.FormPost;
public bool GetClaimsFromUserInfoEndpoint { get; set; }
+ public string AdditionalScopes { get; set; }
+ public string AdditionalUserIdClaimTypes { get; set; }
+ public string AdditionalEmailClaimTypes { get; set; }
+ public string AdditionalNameClaimTypes { get; set; }
+ public string AcrValues { get; set; }
// SAML2 IDP
public string IdpEntityId { get; set; }
@@ -67,6 +74,30 @@ namespace Bit.Core.Models.Data
return BuildSaml2ModulePath(ssoUri, scheme);
}
+ public IEnumerable GetAdditionalScopes() => AdditionalScopes?
+ .Split(',')?
+ .Where(c => !string.IsNullOrWhiteSpace(c))?
+ .Select(c => c.Trim()) ??
+ Array.Empty();
+
+ public IEnumerable GetAdditionalUserIdClaimTypes() => AdditionalUserIdClaimTypes?
+ .Split(',')?
+ .Where(c => !string.IsNullOrWhiteSpace(c))?
+ .Select(c => c.Trim()) ??
+ Array.Empty();
+
+ public IEnumerable GetAdditionalEmailClaimTypes() => AdditionalEmailClaimTypes?
+ .Split(',')?
+ .Where(c => !string.IsNullOrWhiteSpace(c))?
+ .Select(c => c.Trim()) ??
+ Array.Empty();
+
+ public IEnumerable GetAdditionalNameClaimTypes() => AdditionalNameClaimTypes?
+ .Split(',')?
+ .Where(c => !string.IsNullOrWhiteSpace(c))?
+ .Select(c => c.Trim()) ??
+ Array.Empty();
+
private string BuildSsoUrl(string relativePath, string ssoUri)
{
if (string.IsNullOrWhiteSpace(ssoUri) ||
diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx
index 7d89d89a3..478f37d03 100644
--- a/src/Core/Resources/SharedResources.en.resx
+++ b/src/Core/Resources/SharedResources.en.resx
@@ -604,4 +604,20 @@
Disable personal ownership for organization users
+
+ Additional/Custom Scopes (comma delimited)
+
+
+ Additional/Custom User ID Claim Types (comma delimited)
+
+
+ Additional/Custom Email Claim Types (comma delimited)
+
+
+ Additional/Custom Name Claim Types (comma delimited)
+
+
+ Requested Authentication Context Class Reference values (acr_values)
+ 'acr_values' is an explicit OIDC param, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. It should not be translated.
+
diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs
index e2ab4c0b4..fdedf118e 100644
--- a/src/Core/Utilities/CoreHelpers.cs
+++ b/src/Core/Utilities/CoreHelpers.cs
@@ -810,5 +810,15 @@ namespace Bit.Core.Utilities
return System.Text.Json.JsonSerializer.Deserialize(jsonData, options);
}
+
+ public static ICollection AddIfNotExists(this ICollection list, T item)
+ {
+ if (list.Contains(item))
+ {
+ return list;
+ }
+ list.Add(item);
+ return list;
+ }
}
}