I have a basic MVC 5 site setup for user accounts and external logins. For external logins I am using on-premises ADFS OpenID Connect for employees so it should be similar to Azure AD. So in this case the MVC Identity 2.0 verbiage of External Login is actually for Internal Users (Employees). Employees will use ADFS and the public will have user accounts in the aspnet database.
The normal workflow in the MVC template, adds external users to the aspnet database. The workflow adds them or checks if the users are in the appropriate tables in this database. I want to disconnect this database for employees since they are already authenticated by ADFS because I can authorize them using our AD and claims to there is no reason to add them to the database.
Here is the standard code in the AccountController.
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
// Sign in the user with this external login provider if the user already has a login
//var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
var result = await SignInManager.ExternalSignInAsync(loginInfo, isPersistent: false);
switch (result)
{
case SignInStatus.Success:
return RedirectToLocal(returnUrl);
case SignInStatus.LockedOut:
return View("Lockout");
case SignInStatus.RequiresVerification:
return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = false });
case SignInStatus.Failure:
default:
// If the user does not have an account, then prompt the user to create an account
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = loginInfo.Email });
}
}
Here the loginInfo shows the user is authenticated.
The result variable is false since the user is not in the aspnet database and so would send the user to the ExternalLoginConfirmation view.
If I modify the switch statement to send the user to the returnUrl instead, the Authenticate attribute on that controller action sees the user as not authenticated and starts the process over again since the user is not authenticated according to the built-in workflow.
How do I intercept this workflow and satisfy the Authorize attribute? I have checked these properties and they return false.
var authenticated = HttpContext.User.Identity.IsAuthenticated;
var authenticated2 = HttpContext.Request.IsAuthenticated;
var authenticated3 = HttpContext.GetOwinContext().Authentication;
I was able to disconnect the aspnet database from the workflow by removing the code which checks the database tables.
The new code signs out of the external cookie, generates a new application cookie and copies the claims from the external cookie to the application cookie.
public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
{
var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
if (loginInfo == null)
{
return RedirectToAction("Login");
}
//We signed in with an external provider which creates and external cookie but this is not sufficient for application
//authorization so we need to create an application cookie and copy the existing claims and add new claims as needed
var externalClaims = loginInfo.ExternalIdentity.Claims;//get the current external claims for the user
AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);//sign out of external authentication
var claims = externalClaims.ToList();//create a new list of the external claims
//add the identityprovider claim since this is needed for the AntiForgeryToken
claims.Add(new Claim("http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider", loginInfo.DefaultUserName));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie, ClaimTypes.Name, ClaimTypes.Role);//create new identity
AuthenticationManager.SignIn(identity);//sign in with the new local identity containing an application cookie
ViewBag.ReturnUrl = returnUrl;
ViewBag.LoginProvider = loginInfo.Login.LoginProvider;
return RedirectToLocal(returnUrl);
}