@@ -39,6 +39,7 @@ public class BackOfficeController : SecurityControllerBase
3939 private readonly ILogger < BackOfficeController > _logger ;
4040 private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions ;
4141 private readonly IUserTwoFactorLoginService _userTwoFactorLoginService ;
42+ private readonly IBackOfficeExternalLoginProviders _backOfficeExternalLoginProviders ;
4243
4344 public BackOfficeController (
4445 IHttpContextAccessor httpContextAccessor ,
@@ -47,7 +48,8 @@ public BackOfficeController(
4748 IOptions < SecuritySettings > securitySettings ,
4849 ILogger < BackOfficeController > logger ,
4950 IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions ,
50- IUserTwoFactorLoginService userTwoFactorLoginService )
51+ IUserTwoFactorLoginService userTwoFactorLoginService ,
52+ IBackOfficeExternalLoginProviders backOfficeExternalLoginProviders )
5153 {
5254 _httpContextAccessor = httpContextAccessor ;
5355 _backOfficeSignInManager = backOfficeSignInManager ;
@@ -56,6 +58,7 @@ public BackOfficeController(
5658 _logger = logger ;
5759 _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions ;
5860 _userTwoFactorLoginService = userTwoFactorLoginService ;
61+ _backOfficeExternalLoginProviders = backOfficeExternalLoginProviders ;
5962 }
6063
6164 [ HttpPost ( "login" ) ]
@@ -184,6 +187,145 @@ public async Task<IActionResult> Signout(CancellationToken cancellationToken)
184187 return SignOut ( Constants . Security . BackOfficeAuthenticationType , OpenIddictServerAspNetCoreDefaults . AuthenticationScheme ) ;
185188 }
186189
190+ /// <summary>
191+ /// Called when a user links an external login provider in the back office
192+ /// </summary>
193+ /// <param name="provider"></param>
194+ /// <returns></returns>
195+ [ HttpPost ( "link-login" ) ]
196+ [ MapToApiVersion ( "1.0" ) ]
197+ public IActionResult LinkLogin ( string provider )
198+ {
199+ // Request a redirect to the external login provider to link a login for the current user
200+ var redirectUrl = Url . Action ( nameof ( ExternalLinkLoginCallback ) , this . GetControllerName ( ) ) ;
201+
202+ // Configures the redirect URL and user identifier for the specified external login including xsrf data
203+ AuthenticationProperties properties =
204+ _backOfficeSignInManager . ConfigureExternalAuthenticationProperties ( provider , redirectUrl , _backOfficeUserManager . GetUserId ( User ) ) ;
205+
206+ return Challenge ( properties , provider ) ;
207+ }
208+
209+ /// <summary>
210+ /// Callback path when the user initiates a link login request from the back office to the external provider from the
211+ /// <see cref="LinkLogin(string)" /> action
212+ /// </summary>
213+ /// <remarks>
214+ /// An example of this is here
215+ /// https://github.com/dotnet/aspnetcore/blob/main/src/Identity/samples/IdentitySample.Mvc/Controllers/AccountController.cs#L155
216+ /// which this is based on
217+ /// </remarks>
218+ [ HttpGet ( "ExternalLinkLoginCallback" ) ]
219+ [ AllowAnonymous ]
220+ [ MapToApiVersion ( "1.0" ) ]
221+ public async Task < IActionResult > ExternalLinkLoginCallback ( )
222+ {
223+ var cookieAuthenticatedUserAttempt =
224+ await HttpContext . AuthenticateAsync ( Constants . Security . BackOfficeAuthenticationType ) ;
225+
226+ if ( cookieAuthenticatedUserAttempt . Succeeded == false )
227+ {
228+ return Redirect ( _securitySettings . Value . AuthorizeCallbackErrorPathName . AppendQueryStringToUrl (
229+ "flow=external-login-callback" ,
230+ "status=unauthorized" ) ) ;
231+ }
232+
233+ BackOfficeIdentityUser ? user = await _backOfficeUserManager . GetUserAsync ( cookieAuthenticatedUserAttempt . Principal ) ;
234+ if ( user == null )
235+ {
236+ return Redirect ( _securitySettings . Value . AuthorizeCallbackErrorPathName . AppendQueryStringToUrl (
237+ "flow=external-login-callback" ,
238+ "status=user-not-found" ) ) ;
239+ }
240+
241+ ExternalLoginInfo ? info =
242+ await _backOfficeSignInManager . GetExternalLoginInfoAsync ( ) ;
243+
244+ if ( info == null )
245+ {
246+ return Redirect ( _securitySettings . Value . AuthorizeCallbackErrorPathName . AppendQueryStringToUrl (
247+ "flow=external-login-callback" ,
248+ "status=external-info-not-found" ) ) ;
249+ }
250+
251+ IdentityResult addLoginResult = await _backOfficeUserManager . AddLoginAsync ( user , info ) ;
252+ if ( addLoginResult . Succeeded )
253+ {
254+ // Update any authentication tokens if succeeded
255+ await _backOfficeSignInManager . UpdateExternalAuthenticationTokensAsync ( info ) ;
256+ return Redirect ( "/umbraco" ) ; // todo shouldn't this come from configuration
257+ }
258+
259+ // Add errors and redirect for it to be displayed
260+ // TempData[ViewDataExtensions.TokenExternalSignInError] = addLoginResult.Errors;
261+ // return RedirectToLogin(new { flow = "external-login", status = "failed", logout = "true" });
262+ // todo
263+ return Redirect ( _securitySettings . Value . AuthorizeCallbackErrorPathName . AppendQueryStringToUrl (
264+ "flow=external-login-callback" ,
265+ "status=failed" ) ) ;
266+ }
267+
268+ // todo cleanup unhappy responses
269+ [ HttpPost ( "unlink-login" ) ]
270+ [ MapToApiVersion ( "1.0" ) ]
271+ public async Task < IActionResult > PostUnLinkLogin ( UnLinkLoginRequestModel unlinkLoginRequestModel )
272+ {
273+ var userId = User . Identity ? . GetUserId ( ) ;
274+ if ( userId is null )
275+ {
276+ throw new InvalidOperationException ( "Could not find userId" ) ;
277+ }
278+
279+ BackOfficeIdentityUser ? user = await _backOfficeUserManager . FindByIdAsync ( userId ) ;
280+ if ( user == null )
281+ {
282+ throw new InvalidOperationException ( "Could not find user" ) ;
283+ }
284+
285+ AuthenticationScheme ? authType = ( await _backOfficeSignInManager . GetExternalAuthenticationSchemesAsync ( ) )
286+ . FirstOrDefault ( x => x . Name == unlinkLoginRequestModel . LoginProvider ) ;
287+
288+ if ( authType == null )
289+ {
290+ _logger . LogWarning ( "Could not find the supplied external authentication provider" ) ;
291+ }
292+ else
293+ {
294+ BackOfficeExternaLoginProviderScheme ? opt = await _backOfficeExternalLoginProviders . GetAsync ( authType . Name ) ;
295+ if ( opt == null )
296+ {
297+ return StatusCode ( StatusCodes . Status400BadRequest , new ProblemDetailsBuilder ( )
298+ . WithTitle ( "Missing Authentication options" )
299+ . WithDetail ( $ "Could not find external authentication options registered for provider { authType . Name } ")
300+ . Build ( ) ) ;
301+ }
302+
303+ if ( ! opt . ExternalLoginProvider . Options . AutoLinkOptions . AllowManualLinking )
304+ {
305+ // If AllowManualLinking is disabled for this provider we cannot unlink
306+ return StatusCode ( StatusCodes . Status400BadRequest , new ProblemDetailsBuilder ( )
307+ . WithTitle ( "Unlinking disabled" )
308+ . WithDetail ( $ "Manual linking is disabled for provider { authType . Name } ")
309+ . Build ( ) ) ;
310+ }
311+ }
312+
313+ IdentityResult result = await _backOfficeUserManager . RemoveLoginAsync (
314+ user ,
315+ unlinkLoginRequestModel . LoginProvider ,
316+ unlinkLoginRequestModel . ProviderKey ) ;
317+
318+ if ( result . Succeeded )
319+ {
320+ await _backOfficeSignInManager . SignInAsync ( user , true ) ;
321+ return Ok ( ) ;
322+ }
323+
324+ return StatusCode ( StatusCodes . Status400BadRequest , new ProblemDetailsBuilder ( )
325+ . WithTitle ( "Unlinking failed" )
326+ . Build ( ) ) ;
327+ }
328+
187329 /// <summary>
188330 /// Retrieve the user principal stored in the authentication cookie.
189331 /// </summary>
0 commit comments