Integrate ASP.NET Core Identity with Microsoft account

In the previous article
you learned to use Microsoft Account as an external login to your ASP.NET Core
web apps. In that article you didn't use ASP.NET Core Identity in any way. The
Microsoft Account alone was used in the authentication process without any local
account. At times you may want to integrate the Microsoft account with a local
account.
Suppose you want to implement role based security in your web app. The
authentication will happen using Microsoft account but you further want to grant
access based on roles that are local to the web app. In such cases, you are
integrate ASP.NET Code Identity with Microsoft account. The application roles
and user to role mappings will reside in the ASP.NET Core Identity. Upon
successful sign in with Microsoft account you will create a local account that
links to the Microsoft account. The local account will have necessary role
information. Once you sign in using Microsoft account the role based security is
provided by ASP.NET Core Identity.
In this article you will modify the
previous example to use
ASP.NET Core Identity. So, make sure you have previous code ready in Visual
Studio. I am also going to assume that you have the necessary tables ready in a
SQL Server database used by
ASP.NET Core Identity.
Let's get started!
Open Startup class and modify the ConfigureServices() method as shown below:
public void ConfigureServices(
IServiceCollection services)
{
services.AddControllersWithViews();
services.AddRazorPages();
services.AddDbContext<ApplicationDbContext>
(options => options.UseSqlServer(
Configuration.GetConnectionString
("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddIdentity<IdentityUser,IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddAuthentication()
.AddMicrosoftAccount(o =>
{
o.ClientId = Configuration
["Authentication:ClientId"];
o.ClientSecret = Configuration
["Authentication:ClientSecret"];
});
}
In this code, we first registered the ApplicationDbContext that is
responsible for data access required by ASP.NET Core Identity. The
ApplicationDbContext class looks like this:
public class ApplicationDbContext :
IdentityDbContext
{
public ApplicationDbContext
(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
The DefaultConnection is the database connection string stored in
appsettings.json file:
"ConnectionStrings": {
"DefaultConnection": "data source=.;initial
catalog=northwind111;integrated security=true"
}
Make sure to modify the connection string as per your database environment.
Then the code calls AddIdentity() and AddEntityFrameworkStores() methods.
These methods use IdentityUser, IdentityRole, and ApplicationDbContext classes.
The IdentityUser and IdentityRole classes are provided by ASP.NET Core Identity
itself.
Then we call AddAuthentication() and AddMicrosoftAccount() methods. If you
followed the earlier article,
these calls should look familiar to you.
Next, open the HomeController and add a constructor to it as shown below:
public class HomeController : Controller
{
private readonly SignInManager<IdentityUser>
signInManager;
private readonly UserManager<IdentityUser>
userManager;
private readonly RoleManager<IdentityRole>
roleManager;
public HomeController(
SignInManager<IdentityUser> signInManager,
UserManager<IdentityUser> userManager,
RoleManager<IdentityRole> roleManager)
{
this.signInManager = signInManager;
this.userManager = userManager;
this.roleManager = roleManager;
}
...
...
}
We declare a few member variables inside the HomeController class -
SignInManager, UserManager, and RoleManager. These objects are injected in the
constructor and are used by the code we are going to write soon.
Now, go to SignIn() action and modify it as follows:
public IActionResult SignIn()
{
var properties = signInManager.
ConfigureExternalAuthenticationProperties
(MicrosoftAccountDefaults.AuthenticationScheme,
"/Home/SignInSuccess");
return Challenge(properties,
MicrosoftAccountDefaults.AuthenticationScheme);
}
Here, we call the ConfigureExternalAuthenticationProperties() method of
SignInManager to configure an AuthenticationProperties object. Then we pass
AuthenticationProperties object and the authentication scheme to the Challenge()
method.
Upon successful sign-in the control will go to SignInSuccess() action. This
action is shown below:
public async Task<IActionResult> SignInSuccess()
{
var info = await signInManager.GetExternalLoginInfoAsync();
var result = await signInManager.
ExternalLoginSignInAsync(info.LoginProvider,
info.ProviderKey,
isPersistent: false,
bypassTwoFactor: true);
if (result.Succeeded)
{
return RedirectToAction("Index");
}
else
{
var email = info.Principal.FindFirstValue
(ClaimTypes.Email);
var user = new IdentityUser
{
UserName = email,
Email = email
};
var userResult = await userManager.CreateAsync(user);
if (userResult.Succeeded)
{
userResult = await userManager.
AddLoginAsync(user, info);
if(!await roleManager.RoleExistsAsync("Admin"))
{
IdentityRole role = new IdentityRole("Admin");
await roleManager.CreateAsync(role);
}
await userManager.AddToRoleAsync(user, "Admin");
if (userResult.Succeeded)
{
await signInManager.SignInAsync(user,
isPersistent: false,
info.LoginProvider);
return RedirectToAction("Index");
}
}
return RedirectToAction("Index");
}
}
The code calls GetExternalLoginInfoAsync() method of SignInManager to
retrieve external login information such as login provider and its key.
We then call ExternalLoginSignInAsync() method by passing provider name and
key. Initially this method will fail because there is no associated local login
yet.
If there is no local account for this user, we need to create it. This is
done using CreateAsync() method of UserManager. The CreateAsync() method accepts
an IdentityUser object representing the new user. Notice how user name and email
are obtained from ExternalLoginInfo object.
After the user account is created successfully, we call AddLoginAsync()
method to add an external login entry in the Identity store. You will find that
CreateAsync() adds an entry into the AspNetUsers table whereas AddLoginAsync()
adds an entry to AspNetUserLogins table.

We then proceed to check whether Admin role exists in the database or not. If
it doesn't exist, we create it using CreateAsync() method of RoleManager. Once
the role is created, we add the user to Admin role using AddToRoleAsync() method
of UserManager.
Then we sign the user using SignInAsync() method of SignInManager. Finally,
we redirect the control to Index page.

No test whether the role based security is working as expected modify the
Privacy() action like this:
[Authorize(Roles = "Admin")]
public IActionResult Privacy()
{
return View();
}
Now the [Authorize] attribute specifies that the user must belong to Admin
role.
The final change is in the SignOut() action. Earlier we used
HttpContext.SignOutAsync() method. Now we will use SignOutAsync() of
SignInManager.
public async Task<IActionResult> SignOut(string signOutType)
{
if (signOutType == "app")
{
await signInManager.SignOutAsync();
}
...
...
}
Set a break point in the SignInSuccess() action and run the application.
Check the execution step-by-step to understand how the local account gets
created and role is assigned to the user.
That's it for now! Keep coding!!