mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 16:42:50 -05:00
Merge SSO and Portal projects
This commit is contained in:
603
bitwarden_license/src/Sso/Controllers/AccountController.cs
Normal file
603
bitwarden_license/src/Sso/Controllers/AccountController.cs
Normal file
@ -0,0 +1,603 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Table;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using IdentityModel;
|
||||
using IdentityServer4;
|
||||
using IdentityServer4.Extensions;
|
||||
using IdentityServer4.Services;
|
||||
using IdentityServer4.Stores;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Models.Api;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using System.Net;
|
||||
|
||||
namespace Bit.Sso.Controllers
|
||||
{
|
||||
public class AccountController : Controller
|
||||
{
|
||||
private readonly IAuthenticationSchemeProvider _schemeProvider;
|
||||
private readonly IClientStore _clientStore;
|
||||
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
private readonly ILogger<AccountController> _logger;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly ISsoUserRepository _ssoUserRepository;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly II18nService _i18nService;
|
||||
private readonly UserManager<User> _userManager;
|
||||
|
||||
public AccountController(
|
||||
IAuthenticationSchemeProvider schemeProvider,
|
||||
IClientStore clientStore,
|
||||
IIdentityServerInteractionService interaction,
|
||||
ILogger<AccountController> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
ISsoUserRepository ssoUserRepository,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
II18nService i18nService,
|
||||
UserManager<User> userManager)
|
||||
{
|
||||
_schemeProvider = schemeProvider;
|
||||
_clientStore = clientStore;
|
||||
_interaction = interaction;
|
||||
_logger = logger;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_userRepository = userRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_ssoUserRepository = ssoUserRepository;
|
||||
_userService = userService;
|
||||
_i18nService = i18nService;
|
||||
_userManager = userManager;
|
||||
}
|
||||
|
||||
[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");
|
||||
}
|
||||
|
||||
// Validate organization exists from domain_hint
|
||||
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||
if (organization == null)
|
||||
{
|
||||
return invalidJson("OrganizationNotFoundByIdentifierError");
|
||||
}
|
||||
if (!organization.UseSso)
|
||||
{
|
||||
return invalidJson("SsoNotAllowedForOrganizationError");
|
||||
}
|
||||
|
||||
// Validate SsoConfig exists and is Enabled
|
||||
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
||||
if (ssoConfig == null)
|
||||
{
|
||||
return invalidJson("SsoConfigurationNotFoundForOrganizationError");
|
||||
}
|
||||
if (!ssoConfig.Enabled)
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
// Run scheme validation
|
||||
try
|
||||
{
|
||||
await dynamicScheme.Validate();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var translatedException = _i18nService.GetLocalizedHtmlString(ex.Message);
|
||||
var errorKey = "InvalidSchemeConfigurationError";
|
||||
if (!translatedException.ResourceNotFound)
|
||||
{
|
||||
errorKey = ex.Message;
|
||||
}
|
||||
return invalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
|
||||
}
|
||||
}
|
||||
catch (Exception 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
|
||||
{
|
||||
throw new Exception("No domain_hint provided.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult ExternalChallenge(string scheme, string returnUrl, string state, string userIdentifier)
|
||||
{
|
||||
if (string.IsNullOrEmpty(returnUrl))
|
||||
{
|
||||
returnUrl = "~/";
|
||||
}
|
||||
|
||||
if (!Url.IsLocalUrl(returnUrl) && !_interaction.IsValidReturnUrl(returnUrl))
|
||||
{
|
||||
throw new Exception("invalid return URL");
|
||||
}
|
||||
|
||||
var props = new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = Url.Action(nameof(ExternalCallback)),
|
||||
Items =
|
||||
{
|
||||
// scheme will get serialized into `State` and returned back
|
||||
{ "scheme", scheme },
|
||||
{ "return_url", returnUrl },
|
||||
{ "state", state },
|
||||
{ "user_identifier", userIdentifier },
|
||||
}
|
||||
};
|
||||
|
||||
return Challenge(props, scheme);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> ExternalCallback()
|
||||
{
|
||||
// Read external identity from the temporary cookie
|
||||
var result = await HttpContext.AuthenticateAsync(
|
||||
IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||
if (result?.Succeeded != true)
|
||||
{
|
||||
throw new Exception("External authentication error");
|
||||
}
|
||||
|
||||
// Debugging
|
||||
var externalClaims = result.Principal.Claims.Select(c => $"{c.Type}: {c.Value}");
|
||||
_logger.LogDebug("External claims: {@claims}", externalClaims);
|
||||
|
||||
// Lookup our user and external provider info
|
||||
var (user, provider, providerUserId, claims) = await FindUserFromExternalProviderAsync(result);
|
||||
if (user == null)
|
||||
{
|
||||
// This might be where you might initiate a custom workflow for user registration
|
||||
// in this sample we don't show how that would be done, as our sample implementation
|
||||
// simply auto-provisions new external user
|
||||
var userIdentifier = result.Properties.Items.Keys.Contains("user_identifier") ?
|
||||
result.Properties.Items["user_identifier"] : null;
|
||||
user = await AutoProvisionUserAsync(provider, providerUserId, claims, userIdentifier);
|
||||
}
|
||||
|
||||
if (user != null)
|
||||
{
|
||||
// This allows us to collect any additional claims or properties
|
||||
// for the specific protocols used and store them in the local auth cookie.
|
||||
// this is typically used to store data needed for signout from those protocols.
|
||||
var additionalLocalClaims = new List<Claim>();
|
||||
var localSignInProps = new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = true,
|
||||
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(1)
|
||||
};
|
||||
ProcessLoginCallback(result, additionalLocalClaims, localSignInProps);
|
||||
|
||||
// Issue authentication cookie for user
|
||||
await HttpContext.SignInAsync(new IdentityServerUser(user.Id.ToString())
|
||||
{
|
||||
DisplayName = user.Email,
|
||||
IdentityProvider = provider,
|
||||
AdditionalClaims = additionalLocalClaims.ToArray()
|
||||
}, localSignInProps);
|
||||
}
|
||||
|
||||
// Delete temporary cookie used during external authentication
|
||||
await HttpContext.SignOutAsync(IdentityServerConstants.ExternalCookieAuthenticationScheme);
|
||||
|
||||
// Retrieve return URL
|
||||
var returnUrl = result.Properties.Items["return_url"] ?? "~/";
|
||||
|
||||
// Check if external login is in the context of an OIDC request
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context != null)
|
||||
{
|
||||
if (IsNativeClient(context))
|
||||
{
|
||||
// The client is native, so this change in how to
|
||||
// return the response is for better UX for the end user.
|
||||
HttpContext.Response.StatusCode = 200;
|
||||
HttpContext.Response.Headers["Location"] = string.Empty;
|
||||
return View("Redirect", new RedirectViewModel { RedirectUrl = returnUrl });
|
||||
}
|
||||
}
|
||||
|
||||
return Redirect(returnUrl);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> Logout(string logoutId)
|
||||
{
|
||||
// Build a model so the logged out page knows what to display
|
||||
var (updatedLogoutId, redirectUri, externalAuthenticationScheme) = await GetLoggedOutDataAsync(logoutId);
|
||||
|
||||
if (User?.Identity.IsAuthenticated == true)
|
||||
{
|
||||
// Delete local authentication cookie
|
||||
await HttpContext.SignOutAsync();
|
||||
}
|
||||
|
||||
// HACK: Temporary workaroud for the time being that doesn't try to sign out of OneLogin schemes,
|
||||
// which doesnt support SLO
|
||||
if (externalAuthenticationScheme != null && !externalAuthenticationScheme.Contains("onelogin"))
|
||||
{
|
||||
// Build a return URL so the upstream provider will redirect back
|
||||
// to us after the user has logged out. this allows us to then
|
||||
// complete our single sign-out processing.
|
||||
var url = Url.Action("Logout", new { logoutId = updatedLogoutId });
|
||||
|
||||
// This triggers a redirect to the external provider for sign-out
|
||||
return SignOut(new AuthenticationProperties { RedirectUri = url }, externalAuthenticationScheme);
|
||||
}
|
||||
if (redirectUri != null)
|
||||
{
|
||||
return View("Redirect", new RedirectViewModel { RedirectUrl = redirectUri });
|
||||
}
|
||||
else
|
||||
{
|
||||
return Redirect("~/");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims)>
|
||||
FindUserFromExternalProviderAsync(AuthenticateResult result)
|
||||
{
|
||||
var externalUser = result.Principal;
|
||||
|
||||
// Ensure the NameIdentifier used is not a transient name ID, if so, we need a different attribute
|
||||
// for the user identifier.
|
||||
static bool nameIdIsNotTransient(Claim c) => c.Type == ClaimTypes.NameIdentifier
|
||||
&& (c.Properties == null
|
||||
|| !c.Properties.ContainsKey(SamlPropertyKeys.ClaimFormat)
|
||||
|| c.Properties[SamlPropertyKeys.ClaimFormat] != SamlNameIdFormats.Transient);
|
||||
|
||||
// Try to determine the unique id of the external user (issued by the provider)
|
||||
// the most common claim type for that are the sub claim and the NameIdentifier
|
||||
// depending on the external provider, some other claim type might be used
|
||||
var userIdClaim = externalUser.FindFirst(JwtClaimTypes.Subject) ??
|
||||
externalUser.FindFirst(nameIdIsNotTransient) ??
|
||||
// Some SAML providers may use the `uid` attribute for this
|
||||
// where a transient NameID has been sent in the subject
|
||||
externalUser.FindFirst("uid") ??
|
||||
externalUser.FindFirst("upn") ??
|
||||
externalUser.FindFirst("eppn") ??
|
||||
throw new Exception("Unknown userid");
|
||||
|
||||
// Remove the user id claim so we don't include it as an extra claim if/when we provision the user
|
||||
var claims = externalUser.Claims.ToList();
|
||||
claims.Remove(userIdClaim);
|
||||
|
||||
var provider = result.Properties.Items["scheme"];
|
||||
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("Organization not found or SSO configuration not enabled");
|
||||
}
|
||||
var user = await _userRepository.GetBySsoUserAsync(providerUserId, orgId);
|
||||
|
||||
return (user, provider, providerUserId, claims);
|
||||
}
|
||||
|
||||
private async Task<User> AutoProvisionUserAsync(string provider, string providerUserId,
|
||||
IEnumerable<Claim> claims, string userIdentifier)
|
||||
{
|
||||
var name = GetName(claims);
|
||||
var email = GetEmailAddress(claims);
|
||||
|
||||
Guid? orgId = null;
|
||||
if (Guid.TryParse(provider, out var oId))
|
||||
{
|
||||
orgId = oId;
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: support non-org (server-wide) SSO in the future?
|
||||
throw new Exception($"SSO provider, '{provider}' is not an organization id");
|
||||
}
|
||||
|
||||
User existingUser = null;
|
||||
if (string.IsNullOrWhiteSpace(userIdentifier))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
throw new Exception("Cannot find email claim");
|
||||
}
|
||||
existingUser = await _userRepository.GetByEmailAsync(email);
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytes = System.Convert.FromBase64String(userIdentifier);
|
||||
userIdentifier = System.Text.Encoding.UTF8.GetString(bytes);
|
||||
var split = userIdentifier.Split(",");
|
||||
if (split.Length < 2)
|
||||
{
|
||||
throw new Exception("Invalid user identifier.");
|
||||
}
|
||||
var userId = split[0];
|
||||
var token = split[1];
|
||||
|
||||
var tokenOptions = new TokenOptions();
|
||||
|
||||
var claimedUser = await _userService.GetUserByIdAsync(userId);
|
||||
if (claimedUser != null)
|
||||
{
|
||||
var tokenIsValid = await _userManager.VerifyUserTokenAsync(
|
||||
claimedUser, tokenOptions.PasswordResetTokenProvider, TokenPurposes.LinkSso, token);
|
||||
if (tokenIsValid)
|
||||
{
|
||||
existingUser = claimedUser;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Supplied userId and token did not match.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OrganizationUser orgUser = null;
|
||||
if (orgId.HasValue)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(orgId.Value);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new Exception($"Could not find organization for '{orgId}'");
|
||||
}
|
||||
|
||||
if (existingUser != null)
|
||||
{
|
||||
var orgUsers = await _organizationUserRepository.GetManyByUserAsync(existingUser.Id);
|
||||
orgUser = orgUsers.SingleOrDefault(u => u.OrganizationId == orgId.Value &&
|
||||
u.Status != OrganizationUserStatusType.Invited);
|
||||
}
|
||||
|
||||
if (orgUser == null)
|
||||
{
|
||||
if (organization.Seats.HasValue)
|
||||
{
|
||||
var userCount = await _organizationUserRepository.GetCountByOrganizationIdAsync(orgId.Value);
|
||||
var availableSeats = organization.Seats.Value - userCount;
|
||||
if (availableSeats < 1)
|
||||
{
|
||||
// No seats are available
|
||||
throw new Exception($"No seats available for organization, '{organization.Name}'");
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure user is not already invited to this org
|
||||
var existingOrgUserCount = await _organizationUserRepository.GetCountByOrganizationAsync(
|
||||
orgId.Value, email, false);
|
||||
if (existingOrgUserCount > 0)
|
||||
{
|
||||
throw new Exception($"User, '{email}', has already been invited to this organization, '{organization.Name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
User user = null;
|
||||
if (orgUser == null)
|
||||
{
|
||||
if (existingUser != null)
|
||||
{
|
||||
// TODO: send an email inviting this user to link SSO to their account?
|
||||
throw new Exception("User already exists, please link account to SSO after logging in");
|
||||
}
|
||||
|
||||
// Create user record
|
||||
user = new User
|
||||
{
|
||||
Name = name,
|
||||
Email = email
|
||||
};
|
||||
await _userService.RegisterUserAsync(user);
|
||||
|
||||
if (orgId.HasValue)
|
||||
{
|
||||
// Create organization user record
|
||||
orgUser = new OrganizationUser
|
||||
{
|
||||
OrganizationId = orgId.Value,
|
||||
UserId = user.Id,
|
||||
Type = OrganizationUserType.User,
|
||||
Status = OrganizationUserStatusType.Accepted
|
||||
};
|
||||
await _organizationUserRepository.CreateAsync(orgUser);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Since the user is already a member of this organization, let's link their existing user account
|
||||
user = existingUser;
|
||||
}
|
||||
|
||||
// Create sso user record
|
||||
var ssoUser = new SsoUser
|
||||
{
|
||||
ExternalId = providerUserId,
|
||||
UserId = user.Id,
|
||||
OrganizationId = orgId
|
||||
};
|
||||
await _ssoUserRepository.CreateAsync(ssoUser);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
private string GetEmailAddress(IEnumerable<Claim> claims)
|
||||
{
|
||||
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value) && c.Value.Contains("@"));
|
||||
|
||||
var email = filteredClaims.GetFirstMatch(JwtClaimTypes.Email, ClaimTypes.Email,
|
||||
SamlClaimTypes.Email, "mail", "emailaddress");
|
||||
if (!string.IsNullOrWhiteSpace(email))
|
||||
{
|
||||
return email;
|
||||
}
|
||||
|
||||
var username = filteredClaims.GetFirstMatch(JwtClaimTypes.PreferredUserName,
|
||||
SamlClaimTypes.UserId, "uid");
|
||||
if (!string.IsNullOrWhiteSpace(username))
|
||||
{
|
||||
return username;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private string GetName(IEnumerable<Claim> claims)
|
||||
{
|
||||
var filteredClaims = claims.Where(c => !string.IsNullOrWhiteSpace(c.Value));
|
||||
|
||||
var name = filteredClaims.GetFirstMatch(JwtClaimTypes.Name, ClaimTypes.Name,
|
||||
SamlClaimTypes.DisplayName, SamlClaimTypes.CommonName, "displayname", "cn");
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return name;
|
||||
}
|
||||
|
||||
var givenName = filteredClaims.GetFirstMatch(SamlClaimTypes.GivenName, "givenname", "firstname",
|
||||
"fn", "fname", "nickname");
|
||||
var surname = filteredClaims.GetFirstMatch(SamlClaimTypes.Surname, "sn", "surname", "lastname");
|
||||
var nameParts = new[] { givenName, surname }.Where(p => !string.IsNullOrWhiteSpace(p));
|
||||
if (nameParts.Any())
|
||||
{
|
||||
return string.Join(' ', nameParts);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ProcessLoginCallback(AuthenticateResult externalResult,
|
||||
List<Claim> localClaims, AuthenticationProperties localSignInProps)
|
||||
{
|
||||
// If the external system sent a session id claim, copy it over
|
||||
// so we can use it for single sign-out
|
||||
var sid = externalResult.Principal.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.SessionId);
|
||||
if (sid != null)
|
||||
{
|
||||
localClaims.Add(new Claim(JwtClaimTypes.SessionId, sid.Value));
|
||||
}
|
||||
|
||||
// If the external provider issued an idToken, we'll keep it for signout
|
||||
var idToken = externalResult.Properties.GetTokenValue("id_token");
|
||||
if (idToken != null)
|
||||
{
|
||||
localSignInProps.StoreTokens(
|
||||
new[] { new AuthenticationToken { Name = "id_token", Value = idToken } });
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> GetProviderAsync(string returnUrl)
|
||||
{
|
||||
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
|
||||
if (context?.IdP != null && await _schemeProvider.GetSchemeAsync(context.IdP) != null)
|
||||
{
|
||||
return context.IdP;
|
||||
}
|
||||
var schemes = await _schemeProvider.GetAllSchemesAsync();
|
||||
var providers = schemes.Select(x => x.Name).ToList();
|
||||
return providers.FirstOrDefault();
|
||||
}
|
||||
|
||||
private async Task<(string, string, string)> GetLoggedOutDataAsync(string logoutId)
|
||||
{
|
||||
// Get context information (client name, post logout redirect URI and iframe for federated signout)
|
||||
var logout = await _interaction.GetLogoutContextAsync(logoutId);
|
||||
string externalAuthenticationScheme = null;
|
||||
if (User?.Identity.IsAuthenticated == true)
|
||||
{
|
||||
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;
|
||||
if (idp != null && idp != IdentityServerConstants.LocalIdentityProvider)
|
||||
{
|
||||
var providerSupportsSignout = await HttpContext.GetSchemeSupportsSignOutAsync(idp);
|
||||
if (providerSupportsSignout)
|
||||
{
|
||||
if (logoutId == null)
|
||||
{
|
||||
// If there's no current logout context, we need to create one
|
||||
// this captures necessary info from the current logged in user
|
||||
// before we signout and redirect away to the external IdP for signout
|
||||
logoutId = await _interaction.CreateLogoutContextAsync();
|
||||
}
|
||||
|
||||
externalAuthenticationScheme = idp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (logoutId, logout?.PostLogoutRedirectUri, externalAuthenticationScheme);
|
||||
}
|
||||
|
||||
public bool IsNativeClient(IdentityServer4.Models.AuthorizationRequest context)
|
||||
{
|
||||
return !context.RedirectUri.StartsWith("https", StringComparison.Ordinal)
|
||||
&& !context.RedirectUri.StartsWith("http", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
43
bitwarden_license/src/Sso/Controllers/HomeController.cs
Normal file
43
bitwarden_license/src/Sso/Controllers/HomeController.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using IdentityServer4.Services;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Sso.Models;
|
||||
|
||||
namespace Bit.Sso.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
private readonly IIdentityServerInteractionService _interaction;
|
||||
|
||||
public HomeController(IIdentityServerInteractionService interaction)
|
||||
{
|
||||
_interaction = interaction;
|
||||
}
|
||||
|
||||
[HttpGet("~/alive")]
|
||||
[HttpGet("~/now")]
|
||||
[AllowAnonymous]
|
||||
public DateTime GetAlive()
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
|
||||
[HttpGet("~/Error")]
|
||||
[HttpGet("~/Home/Error")]
|
||||
public async Task<IActionResult> Error(string errorId)
|
||||
{
|
||||
var vm = new ErrorViewModel();
|
||||
|
||||
// retrieve error details from identityserver
|
||||
var message = await _interaction.GetErrorContextAsync(errorId);
|
||||
if (message != null)
|
||||
{
|
||||
vm.Error = message;
|
||||
}
|
||||
|
||||
return View("Error", vm);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user