1111using StsServerIdentity . Models ;
1212using IdentityServer4 . Services ;
1313using IdentityServer4 . Stores ;
14- using IdentityServer4 . Models ;
1514using IdentityModel ;
1615using IdentityServer4 ;
1716using IdentityServer4 . Extensions ;
18- using System . Globalization ;
1917using StsServerIdentity . Services ;
2018using Microsoft . Extensions . Localization ;
2119using StsServerIdentity . Resources ;
2220using System . Reflection ;
21+ using Microsoft . AspNetCore . Authentication ;
2322
2423namespace StsServerIdentity . Controllers
2524{
@@ -35,6 +34,8 @@ public class AccountController : Controller
3534 private readonly IClientStore _clientStore ;
3635 private readonly IPersistedGrantService _persistedGrantService ;
3736 private readonly IStringLocalizer _sharedLocalizer ;
37+ private readonly IAuthenticationSchemeProvider _schemeProvider ;
38+ private readonly IEventService _events ;
3839
3940 public AccountController (
4041 UserManager < ApplicationUser > userManager ,
@@ -45,7 +46,10 @@ public AccountController(
4546 IIdentityServerInteractionService interaction ,
4647 IClientStore clientStore ,
4748 IStringLocalizerFactory factory ,
48- Fido2Storage fido2Storage )
49+ Fido2Storage fido2Storage ,
50+ IAuthenticationSchemeProvider schemeProvider ,
51+ IEventService events )
52+
4953 {
5054 _fido2Storage = fido2Storage ;
5155 _userManager = userManager ;
@@ -55,30 +59,24 @@ public AccountController(
5559 _logger = loggerFactory . CreateLogger < AccountController > ( ) ;
5660 _interaction = interaction ;
5761 _clientStore = clientStore ;
62+ _schemeProvider = schemeProvider ;
63+ _events = events ;
5864
5965 var type = typeof ( SharedResource ) ;
6066 var assemblyName = new AssemblyName ( type . GetTypeInfo ( ) . Assembly . FullName ) ;
6167 _sharedLocalizer = factory . Create ( "SharedResource" , assemblyName . Name ) ;
6268 }
6369
64- //
65- // GET: /Account/Login
6670 [ HttpGet ]
6771 [ AllowAnonymous ]
68- public async Task < IActionResult > Login ( string returnUrl = null )
72+ public async Task < IActionResult > Login ( string returnUrl )
6973 {
70- var context = await _interaction . GetAuthorizationContextAsync ( returnUrl ) ;
71- if ( context ? . IdP != null )
72- {
73- // if IdP is passed, then bypass showing the login screen
74- return ExternalLogin ( context . IdP , returnUrl ) ;
75- }
74+ // build a model so we know what to show on the login page
75+ var vm = await BuildLoginViewModelAsync ( returnUrl ) ;
7676
77- var vm = await BuildLoginViewModelAsync ( returnUrl , context ) ;
78-
79- if ( vm . EnableLocalLogin == false && vm . ExternalProviders . Count ( ) == 1 )
77+ if ( vm . IsExternalLoginOnly )
8078 {
81- // only one option for logging in
79+ // we only have one option for logging in and it's an external provider
8280 return ExternalLogin ( vm . ExternalProviders . First ( ) . AuthenticationScheme , returnUrl ) ;
8381 }
8482
@@ -168,76 +166,19 @@ public IActionResult ErrorEnable2FA()
168166 return View ( ) ;
169167 }
170168
171- private async Task < LoginViewModel > BuildLoginViewModelAsync ( string returnUrl , AuthorizationRequest context )
172- {
173- var loginProviders = ( await _signInManager . GetExternalAuthenticationSchemesAsync ( ) ) . ToList ( ) ;
174- var providers = loginProviders
175- . Where ( x => x . DisplayName != null )
176- . Select ( x => new ExternalProvider
177- {
178- DisplayName = x . DisplayName ,
179- AuthenticationScheme = x . Name
180- } ) ;
181-
182- var allowLocal = true ;
183- if ( context ? . ClientId != null )
184- {
185- var client = await _clientStore . FindEnabledClientByIdAsync ( context . ClientId ) ;
186- if ( client != null )
187- {
188- allowLocal = client . EnableLocalLogin ;
189-
190- if ( client . IdentityProviderRestrictions != null && client . IdentityProviderRestrictions . Any ( ) )
191- {
192- providers = providers . Where ( provider => client . IdentityProviderRestrictions . Contains ( provider . AuthenticationScheme ) ) ;
193- }
194- }
195- }
196-
197- return new LoginViewModel
198- {
199- EnableLocalLogin = allowLocal ,
200- ReturnUrl = returnUrl ,
201- Email = context ? . LoginHint ,
202- ExternalProviders = providers . ToArray ( )
203- } ;
204- }
205-
206- private async Task < LoginViewModel > BuildLoginViewModelAsync ( LoginInputModel model )
207- {
208- var context = await _interaction . GetAuthorizationContextAsync ( model . ReturnUrl ) ;
209- var vm = await BuildLoginViewModelAsync ( model . ReturnUrl , context ) ;
210- vm . Email = model . Email ;
211- vm . RememberLogin = model . RememberLogin ;
212- return vm ;
213- }
214-
215- /// <summary>
216- /// Show logout page
217- /// </summary>
218169 [ HttpGet ]
219170 public async Task < IActionResult > Logout ( string logoutId )
220171 {
221- if ( User . Identity . IsAuthenticated == false )
222- {
223- // if the user is not authenticated, then just show logged out page
224- return await Logout ( new LogoutViewModel { LogoutId = logoutId } ) ;
225- }
172+ // build a model so the logout page knows what to display
173+ var vm = await BuildLogoutViewModelAsync ( logoutId ) ;
226174
227- var context = await _interaction . GetLogoutContextAsync ( logoutId ) ;
228- if ( context ? . ShowSignoutPrompt == false )
175+ if ( vm . ShowLogoutPrompt == false )
229176 {
230- // it's safe to automatically sign-out
231- return await Logout ( new LogoutViewModel { LogoutId = logoutId } ) ;
177+ // if the request for logout was properly authenticated from IdentityServer, then
178+ // we don't need to show the prompt and can just log the user out directly.
179+ return await Logout ( vm ) ;
232180 }
233181
234- // show the logout prompt. this prevents attacks where the user
235- // is automatically signed out by another malicious web page.
236- var vm = new LogoutViewModel
237- {
238- LogoutId = logoutId
239- } ;
240-
241182 return View ( vm ) ;
242183 }
243184
@@ -283,11 +224,14 @@ public async Task<IActionResult> Logout(LogoutViewModel model)
283224
284225 var vm = new LoggedOutViewModel
285226 {
227+ AutomaticRedirectAfterSignOut = AccountOptions . AutomaticRedirectAfterSignOut ,
286228 PostLogoutRedirectUri = logout ? . PostLogoutRedirectUri ,
287- ClientName = logout ? . ClientId ,
288- SignOutIframeUrl = logout ? . SignOutIFrameUrl
229+ ClientName = string . IsNullOrEmpty ( logout ? . ClientName ) ? logout ? . ClientId : logout ? . ClientName ,
230+ SignOutIframeUrl = logout ? . SignOutIFrameUrl ,
231+ LogoutId = model . LogoutId
289232 } ;
290233
234+
291235 await _persistedGrantService . RemoveAllGrantsAsync ( subjectId , "angular2client" ) ;
292236
293237 return View ( "LoggedOut" , vm ) ;
@@ -678,6 +622,133 @@ public async Task<IActionResult> VerifyCode(VerifyCodeViewModel model)
678622 }
679623 }
680624
625+ private async Task < LoginViewModel > BuildLoginViewModelAsync ( string returnUrl )
626+ {
627+ var context = await _interaction . GetAuthorizationContextAsync ( returnUrl ) ;
628+ if ( context ? . IdP != null && await _schemeProvider . GetSchemeAsync ( context . IdP ) != null )
629+ {
630+ var local = context . IdP == IdentityServer4 . IdentityServerConstants . LocalIdentityProvider ;
631+
632+ // this is meant to short circuit the UI and only trigger the one external IdP
633+ var vm = new LoginViewModel
634+ {
635+ EnableLocalLogin = local ,
636+ ReturnUrl = returnUrl ,
637+ Email = context ? . LoginHint ,
638+ } ;
639+
640+ if ( ! local )
641+ {
642+ vm . ExternalProviders = new [ ] { new ExternalProvider { AuthenticationScheme = context . IdP } } ;
643+ }
644+
645+ return vm ;
646+ }
647+
648+ var schemes = await _schemeProvider . GetAllSchemesAsync ( ) ;
649+
650+ var providers = schemes
651+ . Where ( x => x . DisplayName != null )
652+ . Select ( x => new ExternalProvider
653+ {
654+ DisplayName = x . DisplayName ?? x . Name ,
655+ AuthenticationScheme = x . Name
656+ } ) . ToList ( ) ;
657+
658+ var allowLocal = true ;
659+ if ( context ? . Client . ClientId != null )
660+ {
661+ var client = await _clientStore . FindEnabledClientByIdAsync ( context . Client . ClientId ) ;
662+ if ( client != null )
663+ {
664+ allowLocal = client . EnableLocalLogin ;
665+
666+ if ( client . IdentityProviderRestrictions != null && client . IdentityProviderRestrictions . Any ( ) )
667+ {
668+ providers = providers . Where ( provider => client . IdentityProviderRestrictions . Contains ( provider . AuthenticationScheme ) ) . ToList ( ) ;
669+ }
670+ }
671+ }
672+
673+ return new LoginViewModel
674+ {
675+ AllowRememberLogin = AccountOptions . AllowRememberLogin ,
676+ EnableLocalLogin = allowLocal && AccountOptions . AllowLocalLogin ,
677+ ReturnUrl = returnUrl ,
678+ Email = context ? . LoginHint ,
679+ ExternalProviders = providers . ToArray ( )
680+ } ;
681+ }
682+
683+ private async Task < LoginViewModel > BuildLoginViewModelAsync ( LoginInputModel model )
684+ {
685+ var vm = await BuildLoginViewModelAsync ( model . ReturnUrl ) ;
686+ vm . Email = model . Email ;
687+ vm . RememberLogin = model . RememberLogin ;
688+ return vm ;
689+ }
690+
691+ private async Task < LogoutViewModel > BuildLogoutViewModelAsync ( string logoutId )
692+ {
693+ var vm = new LogoutViewModel { LogoutId = logoutId , ShowLogoutPrompt = AccountOptions . ShowLogoutPrompt } ;
694+
695+ if ( User ? . Identity . IsAuthenticated != true )
696+ {
697+ // if the user is not authenticated, then just show logged out page
698+ vm . ShowLogoutPrompt = false ;
699+ return vm ;
700+ }
701+
702+ var context = await _interaction . GetLogoutContextAsync ( logoutId ) ;
703+ if ( context ? . ShowSignoutPrompt == false )
704+ {
705+ // it's safe to automatically sign-out
706+ vm . ShowLogoutPrompt = false ;
707+ return vm ;
708+ }
709+
710+ // show the logout prompt. this prevents attacks where the user
711+ // is automatically signed out by another malicious web page.
712+ return vm ;
713+ }
714+
715+ private async Task < LoggedOutViewModel > BuildLoggedOutViewModelAsync ( string logoutId )
716+ {
717+ // get context information (client name, post logout redirect URI and iframe for federated signout)
718+ var logout = await _interaction . GetLogoutContextAsync ( logoutId ) ;
719+
720+ var vm = new LoggedOutViewModel
721+ {
722+ AutomaticRedirectAfterSignOut = AccountOptions . AutomaticRedirectAfterSignOut ,
723+ PostLogoutRedirectUri = logout ? . PostLogoutRedirectUri ,
724+ ClientName = string . IsNullOrEmpty ( logout ? . ClientName ) ? logout ? . ClientId : logout ? . ClientName ,
725+ SignOutIframeUrl = logout ? . SignOutIFrameUrl ,
726+ LogoutId = logoutId
727+ } ;
728+
729+ if ( User ? . Identity . IsAuthenticated == true )
730+ {
731+ var idp = User . FindFirst ( JwtClaimTypes . IdentityProvider ) ? . Value ;
732+ if ( idp != null && idp != IdentityServer4 . IdentityServerConstants . LocalIdentityProvider )
733+ {
734+ var providerSupportsSignout = await HttpContext . GetSchemeSupportsSignOutAsync ( idp ) ;
735+ if ( providerSupportsSignout )
736+ {
737+ if ( vm . LogoutId == null )
738+ {
739+ // if there's no current logout context, we need to create one
740+ // this captures necessary info from the current logged in user
741+ // before we signout and redirect away to the external IdP for signout
742+ vm . LogoutId = await _interaction . CreateLogoutContextAsync ( ) ;
743+ }
744+
745+ vm . ExternalAuthenticationScheme = idp ;
746+ }
747+ }
748+ }
749+
750+ return vm ;
751+ }
681752 #region Helpers
682753
683754 private void AddErrors ( IdentityResult result )
0 commit comments