@@ -1995,6 +1995,134 @@ await SendRequestAsync(r => r
19951995 Assert . False ( token . IsSuspended ) ;
19961996 }
19971997
1998+ [ Theory ]
1999+ [ InlineData ( false ) ]
2000+ [ InlineData ( true ) ]
2001+ public async Task ExternalLogin_InactiveUser_IsRejectedWithoutChangingAccountOrInvite ( bool hasExistingLink )
2002+ {
2003+ // Arrange
2004+ const string code = "github-inactive-user" ;
2005+ string email = TestOAuthProviderClient . GetEmailAddress ( code ) ;
2006+ var user = new User
2007+ {
2008+ EmailAddress = email ,
2009+ FullName = "Inactive User" ,
2010+ IsActive = false ,
2011+ Roles = new HashSet < string > ( [ AuthorizationRoles . Client , AuthorizationRoles . User ] )
2012+ } ;
2013+ user . ResetVerifyEmailAddressTokenAndExpiration ( TimeProvider ) ;
2014+ if ( hasExistingLink )
2015+ user . AddOAuthAccount ( "github" , code , email ) ;
2016+ await _userRepository . AddAsync ( user , options => options . ImmediateConsistency ( ) ) ;
2017+ var organization = ( await _organizationRepository . GetAllAsync ( ) ) . Documents . First ( ) ;
2018+ var invite = new Invite
2019+ {
2020+ Token = StringExtensions . GetNewToken ( ) ,
2021+ EmailAddress = email ,
2022+ DateAdded = TimeProvider . GetUtcNow ( ) . UtcDateTime
2023+ } ;
2024+ organization . Invites . Add ( invite ) ;
2025+ await _organizationRepository . SaveAsync ( organization , options => options . ImmediateConsistency ( ) ) ;
2026+
2027+ // Act
2028+ await SendRequestAsync ( request => request
2029+ . Post ( )
2030+ . AppendPaths ( "auth" , "github" )
2031+ . Content ( new ExternalAuthInfo
2032+ {
2033+ ClientId = "client-id" ,
2034+ Code = code ,
2035+ InviteToken = invite . Token ,
2036+ RedirectUri = "http://localhost/callback"
2037+ } )
2038+ . StatusCodeShouldBeUnauthorized ( ) ) ;
2039+
2040+ // Assert
2041+ var storedUser = await _userRepository . GetByIdAsync ( user . Id , options => options . ImmediateConsistency ( ) ) ;
2042+ Assert . NotNull ( storedUser ) ;
2043+ Assert . False ( storedUser . IsActive ) ;
2044+ Assert . False ( storedUser . IsEmailAddressVerified ) ;
2045+ Assert . Equal ( user . VerifyEmailAddressToken , storedUser . VerifyEmailAddressToken ) ;
2046+ Assert . Equal ( user . VerifyEmailAddressTokenExpiration , storedUser . VerifyEmailAddressTokenExpiration ) ;
2047+ Assert . Equal ( user . OAuthAccounts . Select ( account => ( account . Provider , account . ProviderUserId , account . Username ) ) ,
2048+ storedUser . OAuthAccounts . Select ( account => ( account . Provider , account . ProviderUserId , account . Username ) ) ) ;
2049+ Assert . Empty ( storedUser . OrganizationIds ) ;
2050+ var tokens = await _tokenRepository . GetByTypeAndUserIdAsync ( TokenType . Authentication , user . Id , options => options . ImmediateConsistency ( ) ) ;
2051+ Assert . Empty ( tokens . Documents ) ;
2052+ var storedOrganization = await _organizationRepository . GetByIdAsync ( organization . Id , options => options . ImmediateConsistency ( ) ) ;
2053+ Assert . NotNull ( storedOrganization ) ;
2054+ Assert . Contains ( storedOrganization . Invites , candidate => candidate . Token == invite . Token ) ;
2055+ }
2056+
2057+ [ Fact ]
2058+ public async Task ExternalLogin_InactiveProviderOwner_DoesNotTransferAccount ( )
2059+ {
2060+ // Arrange
2061+ const string code = "github-inactive-provider-owner" ;
2062+ var owner = new User
2063+ {
2064+ EmailAddress = TestOAuthProviderClient . GetEmailAddress ( code ) ,
2065+ FullName = "Inactive Owner" ,
2066+ IsActive = false
2067+ } ;
2068+ owner . ResetVerifyEmailAddressTokenAndExpiration ( TimeProvider ) ;
2069+ owner . AddOAuthAccount ( "github" , code , owner . EmailAddress ) ;
2070+ await _userRepository . AddAsync ( owner , options => options . ImmediateConsistency ( ) ) ;
2071+ var currentUser = await _userRepository . GetByEmailAddressAsync ( SampleDataService . TEST_ORG_USER_EMAIL ) ;
2072+ Assert . NotNull ( currentUser ) ;
2073+ var originalAccounts = currentUser . OAuthAccounts . Select ( account => ( account . Provider , account . ProviderUserId , account . Username ) ) . ToArray ( ) ;
2074+ long originalTokenCount = ( await _tokenRepository . CountAsync ( ) ) . Total ;
2075+
2076+ // Act
2077+ await SendRequestAsync ( request => request
2078+ . Post ( )
2079+ . AsTestOrganizationUser ( )
2080+ . AppendPaths ( "auth" , "github" )
2081+ . Content ( new ExternalAuthInfo
2082+ {
2083+ ClientId = "client-id" ,
2084+ Code = code ,
2085+ RedirectUri = "http://localhost/callback"
2086+ } )
2087+ . StatusCodeShouldBeUnauthorized ( ) ) ;
2088+
2089+ // Assert
2090+ var storedOwner = await _userRepository . GetByIdAsync ( owner . Id , options => options . ImmediateConsistency ( ) ) ;
2091+ Assert . NotNull ( storedOwner ) ;
2092+ Assert . False ( storedOwner . IsEmailAddressVerified ) ;
2093+ Assert . Equal ( owner . VerifyEmailAddressToken , storedOwner . VerifyEmailAddressToken ) ;
2094+ Assert . Equal ( code , Assert . Single ( storedOwner . OAuthAccounts ) . ProviderUserId ) ;
2095+ var storedCurrentUser = await _userRepository . GetByIdAsync ( currentUser . Id , options => options . ImmediateConsistency ( ) ) ;
2096+ Assert . NotNull ( storedCurrentUser ) ;
2097+ Assert . Equal ( originalAccounts , storedCurrentUser . OAuthAccounts . Select ( account => ( account . Provider , account . ProviderUserId , account . Username ) ) ) ;
2098+ Assert . Equal ( originalTokenCount , ( await _tokenRepository . CountAsync ( ) ) . Total ) ;
2099+ }
2100+
2101+ [ Fact ]
2102+ public async Task GetUserByOAuthProviderAsync_DifferentAccountEntries_DoNotMatch ( )
2103+ {
2104+ var user = await _userRepository . GetByEmailAddressAsync ( SampleDataService . TEST_USER_EMAIL ) ;
2105+ Assert . NotNull ( user ) ;
2106+ user . AddOAuthAccount ( "google" , "shared-subject" , user . EmailAddress ) ;
2107+ user . AddOAuthAccount ( "github" , "other-subject" , user . EmailAddress ) ;
2108+ await _userRepository . SaveAsync ( user , o => o . ImmediateConsistency ( ) ) ;
2109+ Assert . Null ( await _userRepository . GetUserByOAuthProviderAsync ( "github" , "shared-subject" ) ) ;
2110+ Assert . Equal ( user . Id , ( await _userRepository . GetUserByOAuthProviderAsync ( "google" , "shared-subject" ) ) ! . Id ) ;
2111+ }
2112+
2113+ [ Fact ]
2114+ public async Task GetUserByOAuthProviderAsync_DuplicateIdentity_RejectsAmbiguousOwner ( )
2115+ {
2116+ foreach ( string email in new [ ] { SampleDataService . TEST_USER_EMAIL , SampleDataService . TEST_ORG_USER_EMAIL } )
2117+ {
2118+ var user = await _userRepository . GetByEmailAddressAsync ( email ) ;
2119+ Assert . NotNull ( user ) ;
2120+ user . AddOAuthAccount ( "github" , "duplicate-subject" , user . EmailAddress ) ;
2121+ await _userRepository . SaveAsync ( user , o => o . ImmediateConsistency ( ) ) ;
2122+ }
2123+ await Assert . ThrowsAsync < UnauthorizedAccessException > ( ( ) => _userRepository . GetUserByOAuthProviderAsync ( "github" , "duplicate-subject" ) ) ;
2124+ }
2125+
19982126 private async Task AssertExternalLoginAsync ( TokenResult ? result , string providerName , string providerUserId )
19992127 {
20002128 Assert . NotNull ( result ) ;
0 commit comments