@ -57,6 +57,7 @@ public class AccountController : Controller
@@ -57,6 +57,7 @@ public class AccountController : Controller
private readonly IDataProtectorTokenFactory < SsoTokenable > _d ataProtector ;
private readonly IOrganizationDomainRepository _ organizationDomainRepository ;
private readonly IRegisterUserCommand _ registerUserCommand ;
private readonly IFeatureService _f eatureService ;
public AccountController (
IAuthenticationSchemeProvider schemeProvider ,
@ -77,7 +78,8 @@ public class AccountController : Controller
@@ -77,7 +78,8 @@ public class AccountController : Controller
Core . Services . IEventService eventService ,
IDataProtectorTokenFactory < SsoTokenable > dataProtector ,
IOrganizationDomainRepository organizationDomainRepository ,
IRegisterUserCommand registerUserCommand )
IRegisterUserCommand registerUserCommand ,
IFeatureService featureService )
{
_ schemeProvider = schemeProvider ;
_ clientStore = clientStore ;
@ -98,10 +100,11 @@ public class AccountController : Controller
@@ -98,10 +100,11 @@ public class AccountController : Controller
_d ataProtector = dataProtector ;
_ organizationDomainRepository = organizationDomainRepository ;
_ registerUserCommand = registerUserCommand ;
_f eatureService = featureService ;
}
[HttpGet]
public async Task < IActionResult > PreValidate ( string domainHint )
public async Task < IActionResult > PreValidateAsync ( string domainHint )
{
try
{
@ -160,7 +163,7 @@ public class AccountController : Controller
@@ -160,7 +163,7 @@ public class AccountController : Controller
}
[HttpGet]
public async Task < IActionResult > Login ( string returnUrl )
public async Task < IActionResult > LoginAsync ( string returnUrl )
{
var context = await _ interaction . GetAuthorizationContextAsync ( returnUrl ) ;
@ -235,37 +238,69 @@ public class AccountController : Controller
@@ -235,37 +238,69 @@ public class AccountController : Controller
[HttpGet]
public async Task < IActionResult > ExternalCallback ( )
{
// Feature flag (PM-24579): Prevent SSO on existing non-compliant users.
var preventOrgUserLoginIfStatusInvalid =
_f eatureService . IsEnabled ( FeatureFlagKeys . PM24579_PreventSsoOnExistingNonCompliantUsers ) ;
// Read external identity from the temporary cookie
var result = await HttpContext . AuthenticateAsync (
AuthenticationSchemes . BitwardenExternalCookieAuthenticationScheme ) ;
if ( result ? . Succeeded ! = true )
if ( preventOrgUserLoginIfStatusInvalid )
{
throw new Exception ( _ i18nService . T ( "ExternalAuthenticationError" ) ) ;
if ( ! result . Succeeded )
{
throw new Exception ( _ i18nService . T ( "ExternalAuthenticationError" ) ) ;
}
}
else
{
if ( result ? . Succeeded ! = true )
{
throw new Exception ( _ i18nService . T ( "ExternalAuthenticationError" ) ) ;
}
}
// Debugging
var externalClaims = result . Principal . Claims . Select ( c = > $"{c.Type}: {c.Value}" ) ;
_l ogger . LogDebug ( "External claims: {@claims}" , externalClaims ) ;
// See if the user has logged in with this SSO provider before and has already been provisioned.
// This is signified by the user existing in the User table and the SSOUser table for the SSO provider they're using.
var ( user , provider , providerUserId , claims , ssoConfigData ) = await FindUserFromExternalProviderAsync ( result ) ;
// We will look these up as required (lazy resolution) to avoid multiple DB hits.
Organization organization = null ;
OrganizationUser orgUser = null ;
// The user has not authenticated with this SSO provider before.
// They could have an existing Bitwarden account in the User table though.
if ( user = = null )
{
// If we're manually linking to SSO, the user's external identifier will be passed as query string parameter.
var userIdentifier = result . Properties . Items . Keys . Contains ( "user_identifier" ) ?
result . Properties . Items [ "user_identifier" ] : null ;
user = await AutoProvisionUserAsync ( provider , providerUserId , claims , userIdentifier , ssoConfigData ) ;
var userIdentifier = result . Properties . Items . Keys . Contains ( "user_identifier" )
? result . Properties . Items [ "user_identifier" ]
: null ;
var ( provisionedUser , foundOrganization , foundOrCreatedOrgUser ) =
await AutoProvisionUserAsync (
provider ,
providerUserId ,
claims ,
userIdentifier ,
ssoConfigData ) ;
user = provisionedUser ;
if ( preventOrgUserLoginIfStatusInvalid )
{
organization = foundOrganization ;
orgUser = foundOrCreatedOrgUser ;
}
}
// Either the user already authenticated with the SSO provider, or we've just provisioned them.
// Either way, we have associated the SSO login with a Bitwarden user.
// We will now sign the Bitwarden user in.
if ( user ! = null )
if ( preventOrgUserLoginIfStatusInvalid )
{
if ( user = = null ) throw new Exception ( _ i18nService . T ( "UserShouldBeFound" ) ) ;
await PreventOrgUserLoginIfStatusInvalidAsync ( organization , provider , orgUser , user ) ;
// 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.
@ -278,12 +313,41 @@ public class AccountController : Controller
@@ -278,12 +313,41 @@ public class AccountController : Controller
ProcessLoginCallback ( result , additionalLocalClaims , localSignInProps ) ;
// Issue authentication cookie for user
await HttpContext . SignInAsync ( new IdentityServerUser ( user . Id . ToString ( ) )
await HttpContext . SignInAsync (
new IdentityServerUser ( user . Id . ToString ( ) )
{
DisplayName = user . Email ,
IdentityProvider = provider ,
AdditionalClaims = additionalLocalClaims . ToArray ( )
} , localSignInProps ) ;
}
else
{
// Either the user already authenticated with the SSO provider, or we've just provisioned them.
// Either way, we have associated the SSO login with a Bitwarden user.
// We will now sign the Bitwarden user in.
if ( user ! = null )
{
DisplayName = user . Email ,
IdentityProvider = provider ,
AdditionalClaims = additionalLocalClaims . ToArray ( )
} , localSignInProps ) ;
// 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
@ -310,7 +374,7 @@ public class AccountController : Controller
@@ -310,7 +374,7 @@ public class AccountController : Controller
}
[HttpGet]
public async Task < IActionResult > Logout ( string logoutId )
public async Task < IActionResult > LogoutAsync ( string logoutId )
{
// Build a model so the logged out page knows what to display
var ( updatedLogoutId , redirectUri , externalAuthenticationScheme ) = await GetLoggedOutDataAsync ( logoutId ) ;
@ -333,6 +397,7 @@ public class AccountController : Controller
@@ -333,6 +397,7 @@ public class AccountController : Controller
// 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 } ) ;
@ -347,7 +412,8 @@ public class AccountController : Controller
@@ -347,7 +412,8 @@ public class AccountController : Controller
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
/// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records.
/// </summary>
private async Task < ( User user , string provider , string providerUserId , IEnumerable < Claim > claims , SsoConfigurationData config ) >
private async Task < ( User user , string provider , string providerUserId , IEnumerable < Claim > claims ,
SsoConfigurationData config ) >
FindUserFromExternalProviderAsync ( AuthenticateResult result )
{
var provider = result . Properties . Items [ "scheme" ] ;
@ -374,9 +440,10 @@ public class AccountController : Controller
@@ -374,9 +440,10 @@ public class AccountController : Controller
// 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 . TryGetValue ( SamlPropertyKeys . ClaimFormat , out var claimFormat )
| | claimFormat ! = SamlNameIdFormats . Transient ) ;
& & ( c . Properties = = null
| | ! c . Properties . TryGetValue ( SamlPropertyKeys . ClaimFormat ,
out var claimFormat )
| | 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
@ -418,24 +485,20 @@ public class AccountController : Controller
@@ -418,24 +485,20 @@ public class AccountController : Controller
/// <param name="providerUserId">The external identity provider's user identifier.</param>
/// <param name="claims">The claims from the external IdP.</param>
/// <param name="userIdentifier">The user identifier used for manual SSO linking.</param>
/// <param name="config ">The SSO configuration for the organization.</param>
/// <returns>The User to sign in .</returns>
/// <param name="ssoConfigData ">The SSO configuration for the organization.</param>
/// <returns>Guaranteed to return the user to sign in as well as the found organization and org user .</returns>
/// <exception cref="Exception">An exception if the user cannot be provisioned as requested.</exception>
private async Task < User > AutoProvisionUserAsync ( string provider , string providerUserId ,
IEnumerable < Claim > claims , string userIdentifier , SsoConfigurationData config )
private async Task < ( User user , Organization foundOrganization , OrganizationUser foundOrgUser ) >
AutoProvisionUserAsync (
string provider ,
string providerUserId ,
IEnumerable < Claim > claims ,
string userIdentifier ,
SsoConfigurationData ssoConfigData
)
{
var name = GetName ( claims , config . GetAdditionalNameClaimTypes ( ) ) ;
var email = GetEmailAddress ( claims , config . GetAdditionalEmailClaimTypes ( ) ) ;
if ( string . IsNullOrWhiteSpace ( email ) & & providerUserId . Contains ( "@" ) )
{
email = providerUserId ;
}
if ( ! Guid . TryParse ( provider , out var orgId ) )
{
// TODO: support non-org (server-wide) SSO in the future?
throw new Exception ( _ i18nService . T ( "SSOProviderIsNotAnOrgId" , provider ) ) ;
}
var name = GetName ( claims , ssoConfigData . GetAdditionalNameClaimTypes ( ) ) ;
var email = TryGetEmailAddress ( claims , ssoConfigData , providerUserId ) ;
User existingUser = null ;
if ( string . IsNullOrWhiteSpace ( userIdentifier ) )
@ -444,15 +507,19 @@ public class AccountController : Controller
@@ -444,15 +507,19 @@ public class AccountController : Controller
{
throw new Exception ( _ i18nService . T ( "CannotFindEmailClaim" ) ) ;
}
existingUser = await _ userRepository . GetByEmailAsync ( email ) ;
}
else
{
existingUser = await GetUserFromManualLinkingData ( userIdentifier ) ;
existingUser = await GetUserFromManualLinkingDataAsync ( userIdentifier ) ;
}
// Try to find the OrganizationUser if it exists.
var ( organization , orgUser ) = await FindOrganizationUser ( existingUser , email , orgId ) ;
// Try to find the org (we error if we can't find an org)
var organization = await TryGetOrganizationByProviderAsync ( provider ) ;
// Try to find an org user (null org user possible and valid here)
var orgUser = await TryGetOrganizationUserByUserAndOrgOrEmail ( existingUser , organization . Id , email ) ;
//----------------------------------------------------
// Scenario 1: We've found the user in the User table
@ -473,22 +540,22 @@ public class AccountController : Controller
@@ -473,22 +540,22 @@ public class AccountController : Controller
throw new Exception ( _ i18nService . T ( "UserAlreadyExistsInviteProcess" ) ) ;
}
EnsureOrgUserStatusAllowed ( orgUser . Status , organization . DisplayName ( ) ,
allowedStatuses : [ OrganizationUserStatusType . Accepted , OrganizationUserStatusType . Confirmed ] ) ;
EnsureAcceptedOrConfirmedOrgUserStatus ( orgUser . Status , organization . DisplayName ( ) ) ;
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
// authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them).
// We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed
// with authentication.
await CreateSsoUserRecord ( providerUserId , existingUser . Id , orgId , orgUser ) ;
return existingUser ;
await CreateSsoUserRecordAsync ( providerUserId , existingUser . Id , organization . Id , orgUser ) ;
return ( existingUser , organization , orgUser ) ;
}
// Before any user creation - if Org User doesn't exist at this point - make sure there are enough seats to add one
if ( orgUser = = null & & organization . Seats . HasValue )
{
var occupiedSeats = await _ organizationRepository . GetOccupiedSeatCountByOrganizationIdAsync ( organization . Id ) ;
var occupiedSeats =
await _ organizationRepository . GetOccupiedSeatCountByOrganizationIdAsync ( organization . Id ) ;
var initialSeatCount = organization . Seats . Value ;
var availableSeats = initialSeatCount - occupiedSeats . Total ;
if ( availableSeats < 1 )
@ -506,8 +573,10 @@ public class AccountController : Controller
@@ -506,8 +573,10 @@ public class AccountController : Controller
{
if ( organization . Seats . Value ! = initialSeatCount )
{
await _ organizationService . AdjustSeatsAsync ( orgId , initialSeatCount - organization . Seats . Value ) ;
await _ organizationService . AdjustSeatsAsync ( organization . Id ,
initialSeatCount - organization . Seats . Value ) ;
}
_l ogger . LogInformation ( e , "SSO auto provisioning failed" ) ;
throw new Exception ( _ i18nService . T ( "NoSeatsAvailable" , organization . DisplayName ( ) ) ) ;
}
@ -519,7 +588,8 @@ public class AccountController : Controller
@@ -519,7 +588,8 @@ public class AccountController : Controller
var emailDomain = CoreHelpers . GetEmailDomain ( email ) ;
if ( ! string . IsNullOrWhiteSpace ( emailDomain ) )
{
var organizationDomain = await _ organizationDomainRepository . GetDomainByOrgIdAndDomainNameAsync ( orgId , emailDomain ) ;
var organizationDomain =
await _ organizationDomainRepository . GetDomainByOrgIdAndDomainNameAsync ( organization . Id , emailDomain ) ;
emailVerified = organizationDomain ? . VerifiedDate . HasValue ? ? false ;
}
@ -537,7 +607,7 @@ public class AccountController : Controller
@@ -537,7 +607,7 @@ public class AccountController : Controller
// If the organization has 2fa policy enabled, make sure to default jit user 2fa to email
var twoFactorPolicy =
await _ policyRepository . GetByOrganizationIdTypeAsync ( orgId , PolicyType . TwoFactorAuthentication ) ;
await _ policyRepository . GetByOrganizationIdTypeAsync ( organization . Id , PolicyType . TwoFactorAuthentication ) ;
if ( twoFactorPolicy ! = null & & twoFactorPolicy . Enabled )
{
user . SetTwoFactorProviders ( new Dictionary < TwoFactorProviderType , TwoFactorProvider >
@ -560,13 +630,14 @@ public class AccountController : Controller
@@ -560,13 +630,14 @@ public class AccountController : Controller
{
orgUser = new OrganizationUser
{
OrganizationId = orgId ,
OrganizationId = organization . Id ,
UserId = user . Id ,
Type = OrganizationUserType . User ,
Status = OrganizationUserStatusType . Invited
} ;
await _ organizationUserRepository . CreateAsync ( orgUser ) ;
}
//-----------------------------------------------------------------
// Scenario 3: There is already an existing OrganizationUser
// That was established through an invitation. We just need to
@ -579,12 +650,47 @@ public class AccountController : Controller
@@ -579,12 +650,47 @@ public class AccountController : Controller
}
// Create the SsoUser record to link the user to the SSO provider.
await CreateSsoUserRecord ( providerUserId , user . Id , orgId , orgUser ) ;
await CreateSsoUserRecordAsync ( providerUserId , user . Id , organization . Id , orgUser ) ;
return user ;
return ( user , organization , orgUser ) ;
}
private async Task < User > GetUserFromManualLinkingData ( string userIdentifier )
/// <summary>
/// Validates an organization user is allowed to log in via SSO and blocks invalid statuses.
/// Lazily resolves the organization and organization user if not provided.
/// </summary>
/// <param name="organization">The target organization; if null, resolved from provider.</param>
/// <param name="provider">The SSO scheme provider value (organization id as a GUID string).</param>
/// <param name="orgUser">The organization-user record; if null, looked up by user/org or user email for invited users.</param>
/// <param name="user">The user attempting to sign in (existing or newly provisioned).</param>
/// <exception cref="Exception">Thrown if the organization cannot be resolved from provider;
/// the organization user cannot be found; or the organization user status is not allowed.</exception>
private async Task PreventOrgUserLoginIfStatusInvalidAsync (
Organization organization ,
string provider ,
OrganizationUser orgUser ,
User user )
{
// Lazily get organization if not already known
organization ? ? = await TryGetOrganizationByProviderAsync ( provider ) ;
// Lazily get the org user if not already known
orgUser ? ? = await TryGetOrganizationUserByUserAndOrgOrEmail (
user ,
organization . Id ,
user . Email ) ;
if ( orgUser ! = null )
{
EnsureAcceptedOrConfirmedOrgUserStatus ( orgUser . Status , organization . DisplayName ( ) ) ;
}
else
{
throw new Exception ( _ i18nService . T ( "CouldNotFindOrganizationUser" , user . Id , organization . Id ) ) ;
}
}
private async Task < User > GetUserFromManualLinkingDataAsync ( string userIdentifier )
{
User user = null ;
var split = userIdentifier . Split ( "," ) ;
@ -592,6 +698,7 @@ public class AccountController : Controller
@@ -592,6 +698,7 @@ public class AccountController : Controller
{
throw new Exception ( _ i18nService . T ( "InvalidUserIdentifier" ) ) ;
}
var userId = split [ 0 ] ;
var token = split [ 1 ] ;
@ -611,38 +718,73 @@ public class AccountController : Controller
@@ -611,38 +718,73 @@ public class AccountController : Controller
throw new Exception ( _ i18nService . T ( "UserIdAndTokenMismatch" ) ) ;
}
}
return user ;
}
private async Task < ( Organization , OrganizationUser ) > FindOrganizationUser ( User existingUser , string email , Guid orgId )
/// <summary>
/// Tries to get the organization by the provider which is org id for us as we use the scheme
/// to identify organizations - not identity providers.
/// </summary>
/// <param name="provider">Org id string from SSO scheme property</param>
/// <exception cref="Exception">Errors if the provider string is not a valid org id guid or if the org cannot be found by the id.</exception>
private async Task < Organization > TryGetOrganizationByProviderAsync ( string provider )
{
OrganizationUser orgUser = null ;
var organization = await _ organizationRepository . GetByIdAsync ( orgId ) ;
if ( ! Guid . TryParse ( provider , out var organizationId ) )
{
// TODO: support non-org (server-wide) SSO in the future?
throw new Exception ( _ i18nService . T ( "SSOProviderIsNotAnOrgId" , provider ) ) ;
}
var organization = await _ organizationRepository . GetByIdAsync ( organizationId ) ;
if ( organization = = null )
{
throw new Exception ( _ i18nService . T ( "CouldNotFindOrganization" , orgId ) ) ;
throw new Exception ( _ i18nService . T ( "CouldNotFindOrganization" , organization Id ) ) ;
}
return organization ;
}
/// <summary>
/// Attempts to get an <see cref="OrganizationUser"/> for a given organization
/// by first checking for an existing user relationship, and if none is found,
/// by looking up an invited user via their email address.
/// </summary>
/// <param name="user">The existing user entity to be looked up in OrganizationUsers table.</param>
/// <param name="organizationId">Organization id from the provider data.</param>
/// <param name="email">Email to use as a fallback in case of an invited user not in the Org Users
/// table yet.</param>
private async Task < OrganizationUser > TryGetOrganizationUserByUserAndOrgOrEmail (
User user ,
Guid organizationId ,
string email )
{
OrganizationUser orgUser = null ;
// Try to find OrgUser via existing User Id.
// This covers any OrganizationUser state after they have accepted an invite.
if ( existingUser ! = null )
if ( u ser ! = null )
{
var orgUsersByUserId = await _ organizationUserRepository . GetManyByUserAsync ( existingUser . Id ) ;
orgUser = orgUsersByUserId . SingleOrDefault ( u = > u . OrganizationId = = orgId ) ;
var orgUsersByUserId = await _ organizationUserRepository . GetManyByUserAsync ( u ser. Id ) ;
orgUser = orgUsersByUserId . SingleOrDefault ( u = > u . OrganizationId = = organization Id ) ;
}
// If no Org User found by Existing User Id - search all the organization's users via email.
// This covers users who are Invited but haven't accepted their invite yet.
orgUser ? ? = await _ organizationUserRepository . GetByOrganizationEmailAsync ( orgId , email ) ;
orgUser ? ? = await _ organizationUserRepository . GetByOrganizationEmailAsync ( organization Id , email ) ;
return ( organization , orgUser ) ;
return orgUser ;
}
private void EnsureOrgUserStatusAllowed (
private void EnsureAcceptedOrConfirmed OrgUserStatus (
OrganizationUserStatusType status ,
string organizationDisplayName ,
params OrganizationUserStatusType [ ] allowedStatuses )
string organizationDisplayName )
{
// The only permissible org user statuses allowed.
OrganizationUserStatusType [ ] allowedStatuses =
[OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed] ;
// if this status is one of the allowed ones, just return
if ( allowedStatuses . Contains ( status ) )
{
@ -667,7 +809,6 @@ public class AccountController : Controller
@@ -667,7 +809,6 @@ public class AccountController : Controller
}
}
private IActionResult InvalidJson ( string errorMessageKey , Exception ex = null )
{
Response . StatusCode = ex = = null ? 4 0 0 : 5 0 0 ;
@ -679,13 +820,13 @@ public class AccountController : Controller
@@ -679,13 +820,13 @@ public class AccountController : Controller
} ) ;
}
private string GetEmailAddress ( IEnumerable < Claim > claims , IEnumerable < string > additionalClaimTypes )
private string Try GetEmailAddressFromClaim s( IEnumerable < Claim > claims , IEnumerable < string > additionalClaimTypes )
{
var filteredClaims = claims . Where ( c = > ! string . IsNullOrWhiteSpace ( c . Value ) & & c . Value . Contains ( "@" ) ) ;
var email = filteredClaims . GetFirstMatch ( additionalClaimTypes . ToArray ( ) ) ? ?
filteredClaims . GetFirstMatch ( JwtClaimTypes . Email , ClaimTypes . Email ,
SamlClaimTypes . Email , "mail" , "emailaddress" ) ;
filteredClaims . GetFirstMatch ( JwtClaimTypes . Email , ClaimTypes . Email ,
SamlClaimTypes . Email , "mail" , "emailaddress" ) ;
if ( ! string . IsNullOrWhiteSpace ( email ) )
{
return email ;
@ -706,8 +847,8 @@ public class AccountController : Controller
@@ -706,8 +847,8 @@ public class AccountController : Controller
var filteredClaims = claims . Where ( c = > ! string . IsNullOrWhiteSpace ( c . Value ) ) ;
var name = filteredClaims . GetFirstMatch ( additionalClaimTypes . ToArray ( ) ) ? ?
filteredClaims . GetFirstMatch ( JwtClaimTypes . Name , ClaimTypes . Name ,
SamlClaimTypes . DisplayName , SamlClaimTypes . CommonName , "displayname" , "cn" ) ;
filteredClaims . GetFirstMatch ( JwtClaimTypes . Name , ClaimTypes . Name ,
SamlClaimTypes . DisplayName , SamlClaimTypes . CommonName , "displayname" , "cn" ) ;
if ( ! string . IsNullOrWhiteSpace ( name ) )
{
return name ;
@ -725,7 +866,8 @@ public class AccountController : Controller
@@ -725,7 +866,8 @@ public class AccountController : Controller
return null ;
}
private async Task CreateSsoUserRecord ( string providerUserId , Guid userId , Guid orgId , OrganizationUser orgUser )
private async Task CreateSsoUserRecordAsync ( string providerUserId , Guid userId , Guid orgId ,
OrganizationUser orgUser )
{
// Delete existing SsoUser (if any) - avoids error if providerId has changed and the sso link is stale
var existingSsoUser = await _ ssoUserRepository . GetByUserIdOrganizationIdAsync ( orgId , userId ) ;
@ -740,12 +882,7 @@ public class AccountController : Controller
@@ -740,12 +882,7 @@ public class AccountController : Controller
await _ eventService . LogOrganizationUserEventAsync ( orgUser , EventType . OrganizationUser_FirstSsoLogin ) ;
}
var ssoUser = new SsoUser
{
ExternalId = providerUserId ,
UserId = userId ,
OrganizationId = orgId ,
} ;
var ssoUser = new SsoUser { ExternalId = providerUserId , UserId = userId , OrganizationId = orgId , } ;
await _ ssoUserRepository . CreateAsync ( ssoUser ) ;
}
@ -769,18 +906,6 @@ public class AccountController : Controller
@@ -769,18 +906,6 @@ public class AccountController : Controller
}
}
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)
@ -812,9 +937,29 @@ public class AccountController : Controller
@@ -812,9 +937,29 @@ public class AccountController : Controller
return ( logoutId , logout ? . PostLogoutRedirectUri , externalAuthenticationScheme ) ;
}
/ * *
* Tries to get a user ' s email from the claims and SSO configuration data or the provider user id if
* the claims email extraction returns null .
* /
private string TryGetEmailAddress (
IEnumerable < Claim > claims ,
SsoConfigurationData config ,
string providerUserId )
{
var email = TryGetEmailAddressFromClaims ( claims , config . GetAdditionalEmailClaimTypes ( ) ) ;
// If email isn't populated from claims and providerUserId has @, assume it is the email.
if ( string . IsNullOrWhiteSpace ( email ) & & providerUserId . Contains ( "@" ) )
{
email = providerUserId ;
}
return email ;
}
public bool IsNativeClient ( DIM . AuthorizationRequest context )
{
return ! context . RedirectUri . StartsWith ( "https" , StringComparison . Ordinal )
& & ! context . RedirectUri . StartsWith ( "http" , StringComparison . Ordinal ) ;
& & ! context . RedirectUri . StartsWith ( "http" , StringComparison . Ordinal ) ;
}
}