1
0
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:
Chad Scharf
2021-02-10 12:00:12 -05:00
committed by GitHub
parent 9f42357705
commit 6cc317c4ba
7 changed files with 181 additions and 51 deletions

View File

@ -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;