mirror of
https://github.com/bitwarden/server.git
synced 2025-04-16 10:38:17 -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:
parent
9f42357705
commit
6cc317c4ba
@ -49,6 +49,11 @@ namespace Bit.Portal.Models
|
|||||||
SpWantAssertionsSigned = configurationData.SpWantAssertionsSigned;
|
SpWantAssertionsSigned = configurationData.SpWantAssertionsSigned;
|
||||||
SpValidateCertificates = configurationData.SpValidateCertificates;
|
SpValidateCertificates = configurationData.SpValidateCertificates;
|
||||||
SpMinIncomingSigningAlgorithm = configurationData.SpMinIncomingSigningAlgorithm ?? SamlSigningAlgorithms.Sha256;
|
SpMinIncomingSigningAlgorithm = configurationData.SpMinIncomingSigningAlgorithm ?? SamlSigningAlgorithms.Sha256;
|
||||||
|
AdditionalScopes = configurationData.AdditionalScopes;
|
||||||
|
AdditionalUserIdClaimTypes = configurationData.AdditionalUserIdClaimTypes;
|
||||||
|
AdditionalEmailClaimTypes = configurationData.AdditionalEmailClaimTypes;
|
||||||
|
AdditionalNameClaimTypes = configurationData.AdditionalNameClaimTypes;
|
||||||
|
AcrValues = configurationData.AcrValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
@ -72,6 +77,16 @@ namespace Bit.Portal.Models
|
|||||||
public OpenIdConnectRedirectBehavior RedirectBehavior { get; set; }
|
public OpenIdConnectRedirectBehavior RedirectBehavior { get; set; }
|
||||||
[Display(Name = "GetClaimsFromUserInfoEndpoint")]
|
[Display(Name = "GetClaimsFromUserInfoEndpoint")]
|
||||||
public bool GetClaimsFromUserInfoEndpoint { get; set; }
|
public bool GetClaimsFromUserInfoEndpoint { get; set; }
|
||||||
|
[Display(Name = "AdditionalScopes")]
|
||||||
|
public string AdditionalScopes { get; set; }
|
||||||
|
[Display(Name = "AdditionalUserIdClaimTypes")]
|
||||||
|
public string AdditionalUserIdClaimTypes { get; set; }
|
||||||
|
[Display(Name = "AdditionalEmailClaimTypes")]
|
||||||
|
public string AdditionalEmailClaimTypes { get; set; }
|
||||||
|
[Display(Name = "AdditionalNameClaimTypes")]
|
||||||
|
public string AdditionalNameClaimTypes { get; set; }
|
||||||
|
[Display(Name = "AcrValues")]
|
||||||
|
public string AcrValues { get; set; }
|
||||||
|
|
||||||
// SAML2 SP
|
// SAML2 SP
|
||||||
[Display(Name = "SpEntityId")]
|
[Display(Name = "SpEntityId")]
|
||||||
@ -218,6 +233,11 @@ namespace Bit.Portal.Models
|
|||||||
SpWantAssertionsSigned = SpWantAssertionsSigned,
|
SpWantAssertionsSigned = SpWantAssertionsSigned,
|
||||||
SpValidateCertificates = SpValidateCertificates,
|
SpValidateCertificates = SpValidateCertificates,
|
||||||
SpMinIncomingSigningAlgorithm = SpMinIncomingSigningAlgorithm,
|
SpMinIncomingSigningAlgorithm = SpMinIncomingSigningAlgorithm,
|
||||||
|
AdditionalScopes = AdditionalScopes,
|
||||||
|
AdditionalUserIdClaimTypes = AdditionalUserIdClaimTypes,
|
||||||
|
AdditionalEmailClaimTypes = AdditionalEmailClaimTypes,
|
||||||
|
AdditionalNameClaimTypes = AdditionalNameClaimTypes,
|
||||||
|
AcrValues = AcrValues,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@
|
|||||||
<h2>@i18nService.T("OpenIdConnectConfig")</h2>
|
<h2>@i18nService.T("OpenIdConnectConfig")</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.CallbackPath">@i18nService.T("CallbackPath")</label>
|
<label asp-for="Data.CallbackPath"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input asp-for="Data.CallbackPath" class="form-control" readonly>
|
<input asp-for="Data.CallbackPath" class="form-control" readonly>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@ -79,7 +79,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SignedOutCallbackPath">@i18nService.T("SignedOutCallbackPath")</label>
|
<label asp-for="Data.SignedOutCallbackPath"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input asp-for="Data.SignedOutCallbackPath" class="form-control" readonly>
|
<input asp-for="Data.SignedOutCallbackPath" class="form-control" readonly>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@ -94,34 +94,34 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.Authority">@i18nService.T("Authority")</label>
|
<label asp-for="Data.Authority"></label>
|
||||||
<input asp-for="Data.Authority" class="form-control">
|
<input asp-for="Data.Authority" class="form-control">
|
||||||
<span asp-validation-for="Data.Authority" class="text-danger"></span>
|
<span asp-validation-for="Data.Authority" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.ClientId">@i18nService.T("ClientId")</label>
|
<label asp-for="Data.ClientId"></label>
|
||||||
<input asp-for="Data.ClientId" class="form-control">
|
<input asp-for="Data.ClientId" class="form-control">
|
||||||
<span asp-validation-for="Data.ClientId" class="text-danger"></span>
|
<span asp-validation-for="Data.ClientId" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.ClientSecret">@i18nService.T("ClientSecret")</label>
|
<label asp-for="Data.ClientSecret"></label>
|
||||||
<input asp-for="Data.ClientSecret" class="form-control">
|
<input asp-for="Data.ClientSecret" class="form-control">
|
||||||
<span asp-validation-for="Data.ClientSecret" class="text-danger"></span>
|
<span asp-validation-for="Data.ClientSecret" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.MetadataAddress">@i18nService.T("MetadataAddress")</label>
|
<label asp-for="Data.MetadataAddress"></label>
|
||||||
<input asp-for="Data.MetadataAddress" class="form-control">
|
<input asp-for="Data.MetadataAddress" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.RedirectBehavior">@i18nService.T("RedirectBehavior")</label>
|
<label asp-for="Data.RedirectBehavior"></label>
|
||||||
<select asp-for="Data.RedirectBehavior" asp-items="Model.RedirectBehaviors"
|
<select asp-for="Data.RedirectBehavior" asp-items="Model.RedirectBehaviors"
|
||||||
class="form-control"></select>
|
class="form-control"></select>
|
||||||
</div>
|
</div>
|
||||||
@ -134,6 +134,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-7 form-group">
|
||||||
|
<label asp-for="Data.AdditionalScopes"></label>
|
||||||
|
<input asp-for="Data.AdditionalScopes" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-7 form-group">
|
||||||
|
<label asp-for="Data.AdditionalUserIdClaimTypes"></label>
|
||||||
|
<input asp-for="Data.AdditionalUserIdClaimTypes" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-7 form-group">
|
||||||
|
<label asp-for="Data.AdditionalEmailClaimTypes"></label>
|
||||||
|
<input asp-for="Data.AdditionalEmailClaimTypes" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-7 form-group">
|
||||||
|
<label asp-for="Data.AdditionalNameClaimTypes"></label>
|
||||||
|
<input asp-for="Data.AdditionalNameClaimTypes" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-7 form-group">
|
||||||
|
<label asp-for="Data.AcrValues"></label>
|
||||||
|
<input asp-for="Data.AcrValues" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -143,7 +173,7 @@
|
|||||||
<h2>@i18nService.T("SamlSpConfig")</h2>
|
<h2>@i18nService.T("SamlSpConfig")</h2>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SpEntityId">@i18nService.T("SpEntityId")</label>
|
<label asp-for="Data.SpEntityId"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input asp-for="Data.SpEntityId" class="form-control" readonly>
|
<input asp-for="Data.SpEntityId" class="form-control" readonly>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@ -158,7 +188,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SpMetadataUrl">@i18nService.T("SpMetadataUrl")</label>
|
<label asp-for="Data.SpMetadataUrl"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input asp-for="Data.SpMetadataUrl" class="form-control" readonly>
|
<input asp-for="Data.SpMetadataUrl" class="form-control" readonly>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@ -182,7 +212,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SpAcsUrl">@i18nService.T("SpAcsUrl")</label>
|
<label asp-for="Data.SpAcsUrl"></label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input asp-for="Data.SpAcsUrl" class="form-control" readonly>
|
<input asp-for="Data.SpAcsUrl" class="form-control" readonly>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
@ -199,28 +229,28 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SpNameIdFormat">@i18nService.T("NameIdFormat")</label>
|
<label asp-for="Data.SpNameIdFormat"></label>
|
||||||
<select asp-for="Data.SpNameIdFormat" asp-items="Model.SpNameIdFormats"
|
<select asp-for="Data.SpNameIdFormat" asp-items="Model.SpNameIdFormats"
|
||||||
class="form-control"></select>
|
class="form-control"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SpOutboundSigningAlgorithm">@i18nService.T("OutboundSigningAlgorithm")</label>
|
<label asp-for="Data.SpOutboundSigningAlgorithm"></label>
|
||||||
<select asp-for="Data.SpOutboundSigningAlgorithm" asp-items="Model.SigningAlgorithms"
|
<select asp-for="Data.SpOutboundSigningAlgorithm" asp-items="Model.SigningAlgorithms"
|
||||||
class="form-control"></select>
|
class="form-control"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SpSigningBehavior">@i18nService.T("SigningBehavior")</label>
|
<label asp-for="Data.SpSigningBehavior"></label>
|
||||||
<select asp-for="Data.SpSigningBehavior" asp-items="Model.SigningBehaviors"
|
<select asp-for="Data.SpSigningBehavior" asp-items="Model.SigningBehaviors"
|
||||||
class="form-control"></select>
|
class="form-control"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.SpMinIncomingSigningAlgorithm">@i18nService.T("MinIncomingSigningAlgorithm")</label>
|
<label asp-for="Data.SpMinIncomingSigningAlgorithm"></label>
|
||||||
<select asp-for="Data.SpMinIncomingSigningAlgorithm" asp-items="Model.SigningAlgorithms"
|
<select asp-for="Data.SpMinIncomingSigningAlgorithm" asp-items="Model.SigningAlgorithms"
|
||||||
class="form-control"></select>
|
class="form-control"></select>
|
||||||
</div>
|
</div>
|
||||||
@ -245,7 +275,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.IdpEntityId">@i18nService.T("EntityId")</label>
|
<label asp-for="Data.IdpEntityId"></label>
|
||||||
<input asp-for="Data.IdpEntityId" class="form-control">
|
<input asp-for="Data.IdpEntityId" class="form-control">
|
||||||
<span asp-validation-for="Data.IdpEntityId" class="text-danger"></span>
|
<span asp-validation-for="Data.IdpEntityId" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
@ -258,14 +288,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.IdpSingleSignOnServiceUrl">@i18nService.T("SingleSignOnServiceUrl")</label>
|
<label asp-for="Data.IdpSingleSignOnServiceUrl"></label>
|
||||||
<input asp-for="Data.IdpSingleSignOnServiceUrl" class="form-control">
|
<input asp-for="Data.IdpSingleSignOnServiceUrl" class="form-control">
|
||||||
<span asp-validation-for="Data.IdpSingleSignOnServiceUrl" class="text-danger"></span>
|
<span asp-validation-for="Data.IdpSingleSignOnServiceUrl" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.IdpSingleLogoutServiceUrl">@i18nService.T("SingleLogoutServiceUrl")</label>
|
<label asp-for="Data.IdpSingleLogoutServiceUrl"></label>
|
||||||
<input asp-for="Data.IdpSingleLogoutServiceUrl" class="form-control">
|
<input asp-for="Data.IdpSingleLogoutServiceUrl" class="form-control">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -278,7 +308,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-7 form-group">
|
<div class="col-7 form-group">
|
||||||
<label asp-for="Data.IdpX509PublicCert">@i18nService.T("X509PublicCert")</label>
|
<label asp-for="Data.IdpX509PublicCert"></label>
|
||||||
<textarea asp-for="Data.IdpX509PublicCert" class="form-control form-control-sm text-monospace" rows="6"></textarea>
|
<textarea asp-for="Data.IdpX509PublicCert" class="form-control form-control-sm text-monospace" rows="6"></textarea>
|
||||||
<span asp-validation-for="Data.IdpX509PublicCert" class="text-danger"></span>
|
<span asp-validation-for="Data.IdpX509PublicCert" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -23,6 +23,8 @@ using System.Threading.Tasks;
|
|||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Sso.Controllers
|
namespace Bit.Sso.Controllers
|
||||||
{
|
{
|
||||||
@ -204,7 +206,7 @@ namespace Bit.Sso.Controllers
|
|||||||
{
|
{
|
||||||
// Read external identity from the temporary cookie
|
// Read external identity from the temporary cookie
|
||||||
var result = await HttpContext.AuthenticateAsync(
|
var result = await HttpContext.AuthenticateAsync(
|
||||||
Core.AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
|
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
|
||||||
if (result?.Succeeded != true)
|
if (result?.Succeeded != true)
|
||||||
{
|
{
|
||||||
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
|
throw new Exception(_i18nService.T("ExternalAuthenticationError"));
|
||||||
@ -215,7 +217,7 @@ namespace Bit.Sso.Controllers
|
|||||||
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
||||||
|
|
||||||
// Lookup our user and external provider info
|
// 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)
|
if (user == null)
|
||||||
{
|
{
|
||||||
// This might be where you might initiate a custom workflow for user registration
|
// 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
|
// simply auto-provisions new external user
|
||||||
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
|
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
|
||||||
result.Properties.Items["user_identifier"] : null;
|
result.Properties.Items["user_identifier"] : null;
|
||||||
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier);
|
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier, ssoConfigData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user != null)
|
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)
|
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;
|
var externalUser = result.Principal;
|
||||||
|
|
||||||
// Ensure the NameIdentifier used is not a transient name ID, if so, we need a different attribute
|
// 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)
|
// 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
|
// 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
|
// 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) ??
|
externalUser.FindFirst(nameIdIsNotTransient) ??
|
||||||
// Some SAML providers may use the `uid` attribute for this
|
// Some SAML providers may use the `uid` attribute for this
|
||||||
// where a transient NameID has been sent in the subject
|
// where a transient NameID has been sent in the subject
|
||||||
@ -333,26 +351,19 @@ namespace Bit.Sso.Controllers
|
|||||||
var claims = externalUser.Claims.ToList();
|
var claims = externalUser.Claims.ToList();
|
||||||
claims.Remove(userIdClaim);
|
claims.Remove(userIdClaim);
|
||||||
|
|
||||||
var provider = result.Properties.Items["scheme"];
|
// find external user
|
||||||
var providerUserId = userIdClaim.Value;
|
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);
|
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,
|
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 name = GetName(claims, config.GetAdditionalNameClaimTypes());
|
||||||
var email = GetEmailAddress(claims);
|
var email = GetEmailAddress(claims, config.GetAdditionalEmailClaimTypes());
|
||||||
if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains("@"))
|
if (string.IsNullOrWhiteSpace(email) && providerUserId.Contains("@"))
|
||||||
{
|
{
|
||||||
email = providerUserId;
|
email = providerUserId;
|
||||||
@ -498,11 +509,12 @@ namespace Bit.Sso.Controllers
|
|||||||
return user;
|
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 filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
|
||||||
|
|
||||||
var email = filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
|
var email = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
|
||||||
|
filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
|
||||||
SamlClaimTypes.Email, "mail", "emailaddress");
|
SamlClaimTypes.Email, "mail", "emailaddress");
|
||||||
if (!string.IsNullOrWhiteSpace(email))
|
if (!string.IsNullOrWhiteSpace(email))
|
||||||
{
|
{
|
||||||
@ -519,11 +531,12 @@ namespace Bit.Sso.Controllers
|
|||||||
return null;
|
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 filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value));
|
||||||
|
|
||||||
var name = filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
|
var name = filteredClaims.GetFirstMatch(additionalClaimTypes.ToArray()) ??
|
||||||
|
filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
|
||||||
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
|
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
|
||||||
if (!string.IsNullOrWhiteSpace(name))
|
if (!string.IsNullOrWhiteSpace(name))
|
||||||
{
|
{
|
||||||
|
@ -10,6 +10,7 @@ using Bit.Core.Models.Data;
|
|||||||
using Bit.Core.Models.Table;
|
using Bit.Core.Models.Table;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Sso;
|
using Bit.Core.Sso;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
using Bit.Sso.Models;
|
using Bit.Sso.Models;
|
||||||
using Bit.Sso.Utilities;
|
using Bit.Sso.Utilities;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
@ -324,21 +325,30 @@ namespace Bit.Core.Business.Sso
|
|||||||
AuthenticationMethod = config.RedirectBehavior,
|
AuthenticationMethod = config.RedirectBehavior,
|
||||||
GetClaimsFromUserInfoEndpoint = config.GetClaimsFromUserInfoEndpoint,
|
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);
|
oidcOptions.Scope.AddIfNotExists(scope);
|
||||||
}
|
|
||||||
if (!oidcOptions.Scope.Contains(OpenIdConnectScopes.Email))
|
|
||||||
{
|
|
||||||
oidcOptions.Scope.Add(OpenIdConnectScopes.Email);
|
|
||||||
}
|
|
||||||
if (!oidcOptions.Scope.Contains(OpenIdConnectScopes.Profile))
|
|
||||||
{
|
|
||||||
oidcOptions.Scope.Add(OpenIdConnectScopes.Profile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
oidcOptions.StateDataFormat = new DistributedCacheStateDataFormatter(_httpContextAccessor, name);
|
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),
|
return new DynamicAuthenticationScheme(name, name, typeof(OpenIdConnectHandler),
|
||||||
oidcOptions, SsoType.OpenIdConnect);
|
oidcOptions, SsoType.OpenIdConnect);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Sso;
|
using Bit.Core.Sso;
|
||||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||||
@ -20,6 +22,11 @@ namespace Bit.Core.Models.Data
|
|||||||
public string MetadataAddress { get; set; }
|
public string MetadataAddress { get; set; }
|
||||||
public OpenIdConnectRedirectBehavior RedirectBehavior { get; set; } = OpenIdConnectRedirectBehavior.FormPost;
|
public OpenIdConnectRedirectBehavior RedirectBehavior { get; set; } = OpenIdConnectRedirectBehavior.FormPost;
|
||||||
public bool GetClaimsFromUserInfoEndpoint { get; set; }
|
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
|
// SAML2 IDP
|
||||||
public string IdpEntityId { get; set; }
|
public string IdpEntityId { get; set; }
|
||||||
@ -67,6 +74,30 @@ namespace Bit.Core.Models.Data
|
|||||||
return BuildSaml2ModulePath(ssoUri, scheme);
|
return BuildSaml2ModulePath(ssoUri, scheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAdditionalScopes() => AdditionalScopes?
|
||||||
|
.Split(',')?
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c))?
|
||||||
|
.Select(c => c.Trim()) ??
|
||||||
|
Array.Empty<string>();
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAdditionalUserIdClaimTypes() => AdditionalUserIdClaimTypes?
|
||||||
|
.Split(',')?
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c))?
|
||||||
|
.Select(c => c.Trim()) ??
|
||||||
|
Array.Empty<string>();
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAdditionalEmailClaimTypes() => AdditionalEmailClaimTypes?
|
||||||
|
.Split(',')?
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c))?
|
||||||
|
.Select(c => c.Trim()) ??
|
||||||
|
Array.Empty<string>();
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAdditionalNameClaimTypes() => AdditionalNameClaimTypes?
|
||||||
|
.Split(',')?
|
||||||
|
.Where(c => !string.IsNullOrWhiteSpace(c))?
|
||||||
|
.Select(c => c.Trim()) ??
|
||||||
|
Array.Empty<string>();
|
||||||
|
|
||||||
private string BuildSsoUrl(string relativePath, string ssoUri)
|
private string BuildSsoUrl(string relativePath, string ssoUri)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(ssoUri) ||
|
if (string.IsNullOrWhiteSpace(ssoUri) ||
|
||||||
|
@ -604,4 +604,20 @@
|
|||||||
<data name="PersonalOwnershipCheckboxDesc" xml:space="preserve">
|
<data name="PersonalOwnershipCheckboxDesc" xml:space="preserve">
|
||||||
<value>Disable personal ownership for organization users</value>
|
<value>Disable personal ownership for organization users</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="AdditionalScopes" xml:space="preserve">
|
||||||
|
<value>Additional/Custom Scopes (comma delimited)</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdditionalUserIdClaimTypes" xml:space="preserve">
|
||||||
|
<value>Additional/Custom User ID Claim Types (comma delimited)</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdditionalEmailClaimTypes" xml:space="preserve">
|
||||||
|
<value>Additional/Custom Email Claim Types (comma delimited)</value>
|
||||||
|
</data>
|
||||||
|
<data name="AdditionalNameClaimTypes" xml:space="preserve">
|
||||||
|
<value>Additional/Custom Name Claim Types (comma delimited)</value>
|
||||||
|
</data>
|
||||||
|
<data name="AcrValues" xml:space="preserve">
|
||||||
|
<value>Requested Authentication Context Class Reference values (acr_values)</value>
|
||||||
|
<comment>'acr_values' is an explicit OIDC param, see https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest. It should not be translated.</comment>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
||||||
|
@ -810,5 +810,15 @@ namespace Bit.Core.Utilities
|
|||||||
|
|
||||||
return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, options);
|
return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ICollection<T> AddIfNotExists<T>(this ICollection<T> list, T item)
|
||||||
|
{
|
||||||
|
if (list.Contains(item))
|
||||||
|
{
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
list.Add(item);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user