mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05:00
SSO - Added custom scopes and claim types for OIDC (#1133)
* SSO - Added custom scopes and claim types for OIDC * Removed redundant field labels * Added acr_values to OIDC config + request
This commit is contained in:
@ -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<Claim> claims)>
|
||||
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> 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<SsoConfigurationData>(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<User> AutoProvisionUserAsync(string provider, string providerUserId,
|
||||
IEnumerable<Claim> claims, string userIdentifier)
|
||||
IEnumerable<Claim> 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<Claim> claims)
|
||||
private string GetEmailAddress(IEnumerable<Claim> claims, IEnumerable<string> 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<Claim> claims)
|
||||
private string GetName(IEnumerable<Claim> claims, IEnumerable<string> 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;
|
||||
|
Reference in New Issue
Block a user