mirror of
https://github.com/bitwarden/server.git
synced 2024-11-25 12:45:18 +01:00
CSA-2 - Require user interaction for SSO redirect (#1948)
* CSA-2 - adding validation before redirecting for SSO login * Updating server to use generated and signed JWT for SSO redirect * Removing erroneous file * Removing erroneous file * Updating for PR feedback, adding domain_hint to Login and fixing invalid domain_hint name reference * Some code styling changes from PR feedback * Removing unnecessary JSON serialization * Couple small changes from PR feedback * Fixing linting errors * Update formatting in AccountController.cs * Remove unused dependency * Add token lifetime to settings * Use tokenable directly * Return defined models * Revert sso proj file changes * Check expiration validity when validating org * Show error message with expired token * Formatting fixes * Add SsoTokenLifetime to Sso settings * Fix build errors * Fix sql warnings Co-authored-by: Carlos J. Muentes <cmuentes@bitwarden.com> Co-authored-by: Chad Scharf <3904944+cscharf@users.noreply.github.com> Co-authored-by: Matt Gibson <mgibson@bitwarden.com>
This commit is contained in:
parent
c27645265c
commit
14302efa2c
@ -8,10 +8,12 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models;
|
using Bit.Core.Models;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Bit.Core.Tokens;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Sso.Models;
|
using Bit.Sso.Models;
|
||||||
using Bit.Sso.Utilities;
|
using Bit.Sso.Utilities;
|
||||||
@ -47,6 +49,7 @@ namespace Bit.Sso.Controllers
|
|||||||
private readonly UserManager<User> _userManager;
|
private readonly UserManager<User> _userManager;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly Core.Services.IEventService _eventService;
|
private readonly Core.Services.IEventService _eventService;
|
||||||
|
private readonly IDataProtectorTokenFactory<SsoTokenable> _dataProtector;
|
||||||
|
|
||||||
public AccountController(
|
public AccountController(
|
||||||
IAuthenticationSchemeProvider schemeProvider,
|
IAuthenticationSchemeProvider schemeProvider,
|
||||||
@ -64,7 +67,8 @@ namespace Bit.Sso.Controllers
|
|||||||
II18nService i18nService,
|
II18nService i18nService,
|
||||||
UserManager<User> userManager,
|
UserManager<User> userManager,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
Core.Services.IEventService eventService)
|
Core.Services.IEventService eventService,
|
||||||
|
IDataProtectorTokenFactory<SsoTokenable> dataProtector)
|
||||||
{
|
{
|
||||||
_schemeProvider = schemeProvider;
|
_schemeProvider = schemeProvider;
|
||||||
_clientStore = clientStore;
|
_clientStore = clientStore;
|
||||||
@ -82,57 +86,47 @@ namespace Bit.Sso.Controllers
|
|||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_dataProtector = dataProtector;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> PreValidate(string domainHint)
|
public async Task<IActionResult> PreValidate(string domainHint)
|
||||||
{
|
{
|
||||||
IActionResult invalidJson(string errorMessageKey, Exception ex = null)
|
|
||||||
{
|
|
||||||
Response.StatusCode = ex == null ? 400 : 500;
|
|
||||||
return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))
|
|
||||||
{
|
|
||||||
ExceptionMessage = ex?.Message,
|
|
||||||
ExceptionStackTrace = ex?.StackTrace,
|
|
||||||
InnerExceptionMessage = ex?.InnerException?.Message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Validate domain_hint provided
|
// Validate domain_hint provided
|
||||||
if (string.IsNullOrWhiteSpace(domainHint))
|
if (string.IsNullOrWhiteSpace(domainHint))
|
||||||
{
|
{
|
||||||
return invalidJson("NoOrganizationIdentifierProvidedError");
|
return InvalidJson("NoOrganizationIdentifierProvidedError");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate organization exists from domain_hint
|
// Validate organization exists from domain_hint
|
||||||
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||||
if (organization == null)
|
if (organization == null)
|
||||||
{
|
{
|
||||||
return invalidJson("OrganizationNotFoundByIdentifierError");
|
return InvalidJson("OrganizationNotFoundByIdentifierError");
|
||||||
}
|
}
|
||||||
if (!organization.UseSso)
|
if (!organization.UseSso)
|
||||||
{
|
{
|
||||||
return invalidJson("SsoNotAllowedForOrganizationError");
|
return InvalidJson("SsoNotAllowedForOrganizationError");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate SsoConfig exists and is Enabled
|
// Validate SsoConfig exists and is Enabled
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
||||||
if (ssoConfig == null)
|
if (ssoConfig == null)
|
||||||
{
|
{
|
||||||
return invalidJson("SsoConfigurationNotFoundForOrganizationError");
|
return InvalidJson("SsoConfigurationNotFoundForOrganizationError");
|
||||||
}
|
}
|
||||||
if (!ssoConfig.Enabled)
|
if (!ssoConfig.Enabled)
|
||||||
{
|
{
|
||||||
return invalidJson("SsoNotEnabledForOrganizationError");
|
return InvalidJson("SsoNotEnabledForOrganizationError");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate Authentication Scheme exists and is loaded (cache)
|
// Validate Authentication Scheme exists and is loaded (cache)
|
||||||
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
|
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
|
||||||
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
|
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
|
||||||
{
|
{
|
||||||
return invalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
|
return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run scheme validation
|
// Run scheme validation
|
||||||
@ -148,37 +142,60 @@ namespace Bit.Sso.Controllers
|
|||||||
{
|
{
|
||||||
errorKey = ex.Message;
|
errorKey = ex.Message;
|
||||||
}
|
}
|
||||||
return invalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
|
return InvalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);
|
||||||
|
var token = _dataProtector.Protect(tokenable);
|
||||||
|
|
||||||
|
return new SsoPreValidateResponseModel(token);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
return invalidJson("PreValidationError", ex);
|
return InvalidJson("PreValidationError", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Everything is good!
|
|
||||||
return new EmptyResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Login(string returnUrl)
|
public async Task<IActionResult> Login(string returnUrl)
|
||||||
{
|
{
|
||||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||||
if (context.Parameters.AllKeys.Contains("domain_hint") &&
|
|
||||||
!string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
|
if (!context.Parameters.AllKeys.Contains("domain_hint") ||
|
||||||
{
|
string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
|
||||||
return RedirectToAction(nameof(ExternalChallenge), new
|
|
||||||
{
|
|
||||||
scheme = context.Parameters["domain_hint"],
|
|
||||||
returnUrl,
|
|
||||||
state = context.Parameters["state"],
|
|
||||||
userIdentifier = context.Parameters["session_state"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
throw new Exception(_i18nService.T("NoDomainHintProvided"));
|
throw new Exception(_i18nService.T("NoDomainHintProvided"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var ssoToken = context.Parameters[SsoTokenable.TokenIdentifier];
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(ssoToken))
|
||||||
|
{
|
||||||
|
return Unauthorized("A valid SSO token is required to continue with SSO login");
|
||||||
|
}
|
||||||
|
|
||||||
|
var domainHint = context.Parameters["domain_hint"];
|
||||||
|
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||||
|
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return InvalidJson("OrganizationNotFoundByIdentifierError");
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenable = _dataProtector.Unprotect(ssoToken);
|
||||||
|
|
||||||
|
if (!tokenable.TokenIsValid(organization))
|
||||||
|
{
|
||||||
|
return Unauthorized("The SSO token associated with your request is expired. A valid SSO token is required to continue.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToAction(nameof(ExternalChallenge), new
|
||||||
|
{
|
||||||
|
scheme = organization.Id.ToString(),
|
||||||
|
returnUrl,
|
||||||
|
state = context.Parameters["state"],
|
||||||
|
userIdentifier = context.Parameters["session_state"],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@ -548,6 +565,17 @@ namespace Bit.Sso.Controllers
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IActionResult InvalidJson(string errorMessageKey, Exception ex = null)
|
||||||
|
{
|
||||||
|
Response.StatusCode = ex == null ? 400 : 500;
|
||||||
|
return Json(new ErrorResponseModel(_i18nService.T(errorMessageKey))
|
||||||
|
{
|
||||||
|
ExceptionMessage = ex?.Message,
|
||||||
|
ExceptionStackTrace = ex?.StackTrace,
|
||||||
|
InnerExceptionMessage = ex?.InnerException?.Message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private string GetEmailAddress(IEnumerable<Claim> claims, IEnumerable<string> additionalClaimTypes)
|
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("@"));
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Bit.Sso.Models
|
||||||
|
{
|
||||||
|
public class SsoPreValidateResponseModel : JsonResult
|
||||||
|
{
|
||||||
|
public SsoPreValidateResponseModel(string token) : base(new
|
||||||
|
{
|
||||||
|
token
|
||||||
|
})
|
||||||
|
{ }
|
||||||
|
}
|
||||||
|
}
|
46
src/Core/Models/Business/Tokenables/SsoTokenable.cs
Normal file
46
src/Core/Models/Business/Tokenables/SsoTokenable.cs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Tokens;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Business.Tokenables
|
||||||
|
{
|
||||||
|
public class SsoTokenable : ExpiringTokenable
|
||||||
|
{
|
||||||
|
public const string ClearTextPrefix = "BWUserPrefix_";
|
||||||
|
public const string DataProtectorPurpose = "SsoTokenDataProtector";
|
||||||
|
public const string TokenIdentifier = "ssoToken";
|
||||||
|
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public string DomainHint { get; set; }
|
||||||
|
public string Identifier { get; set; } = TokenIdentifier;
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public SsoTokenable() { }
|
||||||
|
|
||||||
|
public SsoTokenable(Organization organization, double tokenLifetimeInSeconds) : this()
|
||||||
|
{
|
||||||
|
OrganizationId = organization?.Id ?? default;
|
||||||
|
DomainHint = organization?.Identifier;
|
||||||
|
ExpirationDate = DateTime.UtcNow.AddSeconds(tokenLifetimeInSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TokenIsValid(Organization organization)
|
||||||
|
{
|
||||||
|
if (OrganizationId == default || DomainHint == default || organization == null || !Valid)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return organization.Identifier.Equals(DomainHint, StringComparison.InvariantCultureIgnoreCase)
|
||||||
|
&& organization.Id.Equals(OrganizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validates deserialized
|
||||||
|
protected override bool TokenIsValid() =>
|
||||||
|
Identifier == TokenIdentifier
|
||||||
|
&& OrganizationId != default
|
||||||
|
&& !string.IsNullOrWhiteSpace(DomainHint);
|
||||||
|
}
|
||||||
|
}
|
@ -679,4 +679,10 @@
|
|||||||
<data name="IdpSingleSignOnServiceUrlInvalid" xml:space="preserve">
|
<data name="IdpSingleSignOnServiceUrlInvalid" xml:space="preserve">
|
||||||
<value>Single sign on service URL contains illegal characters.</value>
|
<value>Single sign on service URL contains illegal characters.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SsoRedirectTokenValidationMissing" xml:space="preserve">
|
||||||
|
<value>Single sign on redirect token is missing from the request.</value>
|
||||||
|
</data>
|
||||||
|
<data name="InvalidSsoRedirectToken" xml:space="preserve">
|
||||||
|
<value>Single sign on redirect token is invalid or expired.</value>
|
||||||
|
</data>
|
||||||
</root>
|
</root>
|
@ -67,7 +67,7 @@ namespace Bit.Core.Settings
|
|||||||
public virtual AmazonSettings Amazon { get; set; } = new AmazonSettings();
|
public virtual AmazonSettings Amazon { get; set; } = new AmazonSettings();
|
||||||
public virtual ServiceBusSettings ServiceBus { get; set; } = new ServiceBusSettings();
|
public virtual ServiceBusSettings ServiceBus { get; set; } = new ServiceBusSettings();
|
||||||
public virtual AppleIapSettings AppleIap { get; set; } = new AppleIapSettings();
|
public virtual AppleIapSettings AppleIap { get; set; } = new AppleIapSettings();
|
||||||
public virtual SsoSettings Sso { get; set; } = new SsoSettings();
|
public virtual ISsoSettings Sso { get; set; } = new SsoSettings();
|
||||||
public virtual StripeSettings Stripe { get; set; } = new StripeSettings();
|
public virtual StripeSettings Stripe { get; set; } = new StripeSettings();
|
||||||
public virtual ITwoFactorAuthSettings TwoFactorAuth { get; set; } = new TwoFactorAuthSettings();
|
public virtual ITwoFactorAuthSettings TwoFactorAuth { get; set; } = new TwoFactorAuthSettings();
|
||||||
|
|
||||||
@ -461,9 +461,10 @@ namespace Bit.Core.Settings
|
|||||||
public bool AppInReview { get; set; }
|
public bool AppInReview { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SsoSettings
|
public class SsoSettings : ISsoSettings
|
||||||
{
|
{
|
||||||
public int CacheLifetimeInSeconds { get; set; } = 60;
|
public int CacheLifetimeInSeconds { get; set; } = 60;
|
||||||
|
public double SsoTokenLifetimeInSeconds { get; set; } = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CaptchaSettings
|
public class CaptchaSettings
|
||||||
|
@ -14,5 +14,6 @@
|
|||||||
IConnectionStringSettings Storage { get; set; }
|
IConnectionStringSettings Storage { get; set; }
|
||||||
IBaseServiceUriSettings BaseServiceUri { get; set; }
|
IBaseServiceUriSettings BaseServiceUri { get; set; }
|
||||||
ITwoFactorAuthSettings TwoFactorAuth { get; set; }
|
ITwoFactorAuthSettings TwoFactorAuth { get; set; }
|
||||||
|
ISsoSettings Sso { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
src/Core/Settings/ISsoSettings.cs
Normal file
8
src/Core/Settings/ISsoSettings.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Bit.Core.Settings
|
||||||
|
{
|
||||||
|
public interface ISsoSettings
|
||||||
|
{
|
||||||
|
int CacheLifetimeInSeconds { get; set; }
|
||||||
|
double SsoTokenLifetimeInSeconds { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -3368,4 +3368,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ using System.Security.Claims;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Api;
|
using Bit.Core.Models.Api;
|
||||||
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Identity.Models;
|
using Bit.Identity.Models;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
@ -59,14 +60,11 @@ namespace Bit.Identity.Controllers
|
|||||||
var culture = requestCultureFeature.RequestCulture.Culture.Name;
|
var culture = requestCultureFeature.RequestCulture.Culture.Name;
|
||||||
var requestPath = $"/Account/PreValidate?domainHint={domainHint}&culture={culture}";
|
var requestPath = $"/Account/PreValidate?domainHint={domainHint}&culture={culture}";
|
||||||
var httpClient = _clientFactory.CreateClient("InternalSso");
|
var httpClient = _clientFactory.CreateClient("InternalSso");
|
||||||
|
|
||||||
|
// Forward the internal SSO result
|
||||||
using var responseMessage = await httpClient.GetAsync(requestPath);
|
using var responseMessage = await httpClient.GetAsync(requestPath);
|
||||||
if (responseMessage.IsSuccessStatusCode)
|
|
||||||
{
|
|
||||||
// All is good!
|
|
||||||
return new EmptyResult();
|
|
||||||
}
|
|
||||||
Response.StatusCode = (int)responseMessage.StatusCode;
|
|
||||||
var responseJson = await responseMessage.Content.ReadAsStringAsync();
|
var responseJson = await responseMessage.Content.ReadAsStringAsync();
|
||||||
|
Response.StatusCode = (int)responseMessage.StatusCode;
|
||||||
return Content(responseJson, "application/json");
|
return Content(responseJson, "application/json");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@ -89,6 +87,7 @@ namespace Bit.Identity.Controllers
|
|||||||
|
|
||||||
var domainHint = context.Parameters.AllKeys.Contains("domain_hint") ?
|
var domainHint = context.Parameters.AllKeys.Contains("domain_hint") ?
|
||||||
context.Parameters["domain_hint"] : null;
|
context.Parameters["domain_hint"] : null;
|
||||||
|
var ssoToken = context.Parameters[SsoTokenable.TokenIdentifier];
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(domainHint))
|
if (string.IsNullOrWhiteSpace(domainHint))
|
||||||
{
|
{
|
||||||
@ -100,27 +99,28 @@ namespace Bit.Identity.Controllers
|
|||||||
|
|
||||||
return RedirectToAction(nameof(ExternalChallenge), new
|
return RedirectToAction(nameof(ExternalChallenge), new
|
||||||
{
|
{
|
||||||
organizationIdentifier = domainHint,
|
domainHint = domainHint,
|
||||||
returnUrl,
|
returnUrl,
|
||||||
userIdentifier
|
userIdentifier,
|
||||||
|
ssoToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> ExternalChallenge(string organizationIdentifier, string returnUrl,
|
public async Task<IActionResult> ExternalChallenge(string domainHint, string returnUrl,
|
||||||
string userIdentifier)
|
string userIdentifier, string ssoToken)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(organizationIdentifier))
|
if (string.IsNullOrWhiteSpace(domainHint))
|
||||||
{
|
{
|
||||||
throw new Exception("Invalid organization reference id.");
|
throw new Exception("Invalid organization reference id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(organizationIdentifier);
|
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
||||||
if (ssoConfig == null || !ssoConfig.Enabled)
|
if (ssoConfig == null || !ssoConfig.Enabled)
|
||||||
{
|
{
|
||||||
throw new Exception("Organization not found or SSO configuration not enabled");
|
throw new Exception("Organization not found or SSO configuration not enabled");
|
||||||
}
|
}
|
||||||
var domainHint = ssoConfig.OrganizationId.ToString();
|
var organizationId = ssoConfig.OrganizationId.ToString();
|
||||||
|
|
||||||
var scheme = "sso";
|
var scheme = "sso";
|
||||||
var props = new AuthenticationProperties
|
var props = new AuthenticationProperties
|
||||||
@ -130,8 +130,13 @@ namespace Bit.Identity.Controllers
|
|||||||
{
|
{
|
||||||
{ "return_url", returnUrl },
|
{ "return_url", returnUrl },
|
||||||
{ "domain_hint", domainHint },
|
{ "domain_hint", domainHint },
|
||||||
|
{ "organizationId", organizationId },
|
||||||
{ "scheme", scheme },
|
{ "scheme", scheme },
|
||||||
},
|
},
|
||||||
|
Parameters =
|
||||||
|
{
|
||||||
|
{ "ssoToken", ssoToken },
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(userIdentifier))
|
if (!string.IsNullOrWhiteSpace(userIdentifier))
|
||||||
@ -173,7 +178,7 @@ namespace Bit.Identity.Controllers
|
|||||||
IsPersistent = true,
|
IsPersistent = true,
|
||||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)
|
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)
|
||||||
};
|
};
|
||||||
if (result.Properties != null && result.Properties.Items.TryGetValue("domain_hint", out var organization))
|
if (result.Properties != null && result.Properties.Items.TryGetValue("organizationId", out var organization))
|
||||||
{
|
{
|
||||||
additionalLocalClaims.Add(new Claim("organizationId", organization));
|
additionalLocalClaims.Add(new Claim("organizationId", organization));
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
|||||||
using AspNetCoreRateLimit;
|
using AspNetCoreRateLimit;
|
||||||
using Bit.Core;
|
using Bit.Core;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Identity.Utilities;
|
using Bit.Identity.Utilities;
|
||||||
@ -110,10 +111,17 @@ namespace Bit.Identity
|
|||||||
{
|
{
|
||||||
// Pass domain_hint onto the sso idp
|
// Pass domain_hint onto the sso idp
|
||||||
context.ProtocolMessage.DomainHint = context.Properties.Items["domain_hint"];
|
context.ProtocolMessage.DomainHint = context.Properties.Items["domain_hint"];
|
||||||
|
context.ProtocolMessage.Parameters.Add("organizationId", context.Properties.Items["organizationId"]);
|
||||||
if (context.Properties.Items.ContainsKey("user_identifier"))
|
if (context.Properties.Items.ContainsKey("user_identifier"))
|
||||||
{
|
{
|
||||||
context.ProtocolMessage.SessionState = context.Properties.Items["user_identifier"];
|
context.ProtocolMessage.SessionState = context.Properties.Items["user_identifier"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.Properties.Parameters.Count > 0 && context.Properties.Parameters.ContainsKey(SsoTokenable.TokenIdentifier))
|
||||||
|
{
|
||||||
|
var token = context.Properties.Parameters[SsoTokenable.TokenIdentifier].ToString();
|
||||||
|
context.ProtocolMessage.Parameters.Add(SsoTokenable.TokenIdentifier, token);
|
||||||
|
}
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -121,6 +121,11 @@ namespace Bit.SharedWeb.Utilities
|
|||||||
HCaptchaTokenable.DataProtectorPurpose,
|
HCaptchaTokenable.DataProtectorPurpose,
|
||||||
serviceProvider.GetDataProtectionProvider())
|
serviceProvider.GetDataProtectionProvider())
|
||||||
);
|
);
|
||||||
|
services.AddSingleton<IDataProtectorTokenFactory<SsoTokenable>>(serviceProvider =>
|
||||||
|
new DataProtectorTokenFactory<SsoTokenable>(
|
||||||
|
SsoTokenable.ClearTextPrefix,
|
||||||
|
SsoTokenable.DataProtectorPurpose,
|
||||||
|
serviceProvider.GetDataProtectionProvider()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
|
public static void AddDefaultServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||||
|
@ -3351,4 +3351,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,6 @@ BEGIN
|
|||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationSponsorship] OS
|
[dbo].[OrganizationSponsorship] OS
|
||||||
WHERE
|
WHERE
|
||||||
[SponsoringOrganizationUserId] = @OrganizationUserId
|
[SponsoringOrganizationUserID] = @OrganizationUserId
|
||||||
END
|
END
|
||||||
GO
|
GO
|
||||||
|
@ -11,6 +11,6 @@ BEGIN
|
|||||||
FROM
|
FROM
|
||||||
[dbo].[OrganizationSponsorship] OS
|
[dbo].[OrganizationSponsorship] OS
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
@SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserId
|
@SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserID
|
||||||
END
|
END
|
||||||
GO
|
GO
|
||||||
|
@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using AutoFixture.Xunit2;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.Tokens;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Models.Business.Tokenables
|
||||||
|
{
|
||||||
|
public class SsoTokenableTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CanHandleNullOrganization()
|
||||||
|
{
|
||||||
|
var token = new SsoTokenable(null, default);
|
||||||
|
|
||||||
|
Assert.Equal(default, token.OrganizationId);
|
||||||
|
Assert.Equal(default, token.DomainHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TokenWithNullOrganizationIsInvalid()
|
||||||
|
{
|
||||||
|
var token = new SsoTokenable(null, 500)
|
||||||
|
{
|
||||||
|
ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(token.Valid);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void TokenValidityCheckNullOrganizationIsInvalid(Organization organization)
|
||||||
|
{
|
||||||
|
var token = new SsoTokenable(organization, 500)
|
||||||
|
{
|
||||||
|
ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1)
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.False(token.TokenIsValid(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public void SetsDataFromOrganization(Organization organization)
|
||||||
|
{
|
||||||
|
var token = new SsoTokenable(organization, default);
|
||||||
|
|
||||||
|
Assert.Equal(organization.Id, token.OrganizationId);
|
||||||
|
Assert.Equal(organization.Identifier, token.DomainHint);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetsExpirationFromConstructor()
|
||||||
|
{
|
||||||
|
var expectedDateTime = DateTime.UtcNow.AddSeconds(500);
|
||||||
|
var token = new SsoTokenable(null, 500);
|
||||||
|
|
||||||
|
Assert.Equal(expectedDateTime, token.ExpirationDate, TimeSpan.FromMilliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public void SerializationSetsCorrectDateTime(Organization organization)
|
||||||
|
{
|
||||||
|
var expectedDateTime = DateTime.UtcNow.AddHours(-5);
|
||||||
|
var token = new SsoTokenable(organization, default)
|
||||||
|
{
|
||||||
|
ExpirationDate = expectedDateTime
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = Tokenable.FromToken<HCaptchaTokenable>(token.ToToken());
|
||||||
|
|
||||||
|
Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, AutoData]
|
||||||
|
public void TokenIsValidFailsWhenExpired(Organization organization)
|
||||||
|
{
|
||||||
|
var expectedDateTime = DateTime.UtcNow.AddHours(-5);
|
||||||
|
var token = new SsoTokenable(organization, default)
|
||||||
|
{
|
||||||
|
ExpirationDate = expectedDateTime
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = token.TokenIsValid(organization);
|
||||||
|
|
||||||
|
Assert.False(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3289,4 +3289,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user