1
0
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:
Chad Scharf 2021-02-10 12:00:12 -05:00 committed by GitHub
parent 9f42357705
commit 6cc317c4ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 181 additions and 51 deletions

View File

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

View File

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

View File

@ -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))
{ {

View File

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

View File

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

View File

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

View File

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