mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05: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:

committed by
GitHub

parent
c27645265c
commit
14302efa2c
@ -8,10 +8,12 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business.Tokenables;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
@ -47,6 +49,7 @@ namespace Bit.Sso.Controllers
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly Core.Services.IEventService _eventService;
|
||||
private readonly IDataProtectorTokenFactory<SsoTokenable> _dataProtector;
|
||||
|
||||
public AccountController(
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
@ -64,7 +67,8 @@ namespace Bit.Sso.Controllers
|
||||
II18nService i18nService,
|
||||
UserManager<User> userManager,
|
||||
IGlobalSettings globalSettings,
|
||||
Core.Services.IEventService eventService)
|
||||
Core.Services.IEventService eventService,
|
||||
IDataProtectorTokenFactory<SsoTokenable> dataProtector)
|
||||
{
|
||||
_schemeProvider = schemeProvider;
|
||||
_clientStore = clientStore;
|
||||
@ -82,57 +86,47 @@ namespace Bit.Sso.Controllers
|
||||
_userManager = userManager;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_dataProtector = dataProtector;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
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
|
||||
{
|
||||
// Validate domain_hint provided
|
||||
if (string.IsNullOrWhiteSpace(domainHint))
|
||||
{
|
||||
return invalidJson("NoOrganizationIdentifierProvidedError");
|
||||
return InvalidJson("NoOrganizationIdentifierProvidedError");
|
||||
}
|
||||
|
||||
// Validate organization exists from domain_hint
|
||||
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||
if (organization == null)
|
||||
{
|
||||
return invalidJson("OrganizationNotFoundByIdentifierError");
|
||||
return InvalidJson("OrganizationNotFoundByIdentifierError");
|
||||
}
|
||||
if (!organization.UseSso)
|
||||
{
|
||||
return invalidJson("SsoNotAllowedForOrganizationError");
|
||||
return InvalidJson("SsoNotAllowedForOrganizationError");
|
||||
}
|
||||
|
||||
// Validate SsoConfig exists and is Enabled
|
||||
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
||||
if (ssoConfig == null)
|
||||
{
|
||||
return invalidJson("SsoConfigurationNotFoundForOrganizationError");
|
||||
return InvalidJson("SsoConfigurationNotFoundForOrganizationError");
|
||||
}
|
||||
if (!ssoConfig.Enabled)
|
||||
{
|
||||
return invalidJson("SsoNotEnabledForOrganizationError");
|
||||
return InvalidJson("SsoNotEnabledForOrganizationError");
|
||||
}
|
||||
|
||||
// Validate Authentication Scheme exists and is loaded (cache)
|
||||
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
|
||||
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
|
||||
{
|
||||
return invalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
|
||||
return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
|
||||
}
|
||||
|
||||
// Run scheme validation
|
||||
@ -148,37 +142,60 @@ namespace Bit.Sso.Controllers
|
||||
{
|
||||
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)
|
||||
{
|
||||
return invalidJson("PreValidationError", ex);
|
||||
return InvalidJson("PreValidationError", ex);
|
||||
}
|
||||
|
||||
// Everything is good!
|
||||
return new EmptyResult();
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Login(string returnUrl)
|
||||
{
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
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
|
||||
|
||||
if (!context.Parameters.AllKeys.Contains("domain_hint") ||
|
||||
string.IsNullOrWhiteSpace(context.Parameters["domain_hint"]))
|
||||
{
|
||||
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]
|
||||
@ -548,6 +565,17 @@ namespace Bit.Sso.Controllers
|
||||
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)
|
||||
{
|
||||
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
|
||||
|
Reference in New Issue
Block a user