We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Social Authentication in ASP.NET Core - Getting Started

ASP.NET Core  • Posted 2 months ago

Off late, Authenticating users with social logins has become a standard in web app logins and has become one of the most preferred login methods by the users. Several authentication providers such as Google, Facebook, LinkedIn, Twitter and others to name a few offer secure authentication methods and uses who possess accounts in these login providers can login into the web applications configured to authenticate with these login providers with their respective social login accounts and authorize the requesting web application access to their profile data already created in these login providers.

In this series, let's talk about how we can implement social authentication and provide users with several social login options and how we can handle the user data obtained as a result of these logins in our application. In the hands-on, we'll look at how to setup a login module and then integrate Facebook and Google login authentications to this login module. Finally, we shall create a reusable login module that provides login options on Google and Facebook via a single endpoint.

Setting up the Context - How Authentication Works?

Most of the social authentication providers, implement authentication by means of OAuth2 protocol. OAuth2 or Open Authentication 2 is an opensource authentication protocol, which facilitates authenticating a user over an authentication provider for a requesting party and then securely exchanging user identity, without having the need for user credentials. The authentication flow happens in three steps for a user (u), requesting app (rqa) and login identity provider (idp) as below:

1. u clicks on a login button in rqa and selects a provider idp
2. rqa redirects users to chosen idp and user enters the credentials for idp
3. idp redirects back to rqa on successful authentication and passes an authorization code (au) 
4. rqa requests idp for necessary details about user u in exchange for the authorization code (au)

Learn more about OAuth and how it works in detail.

Having said that, in server side applications such as ASP.NET Core which we're about to use for our implementation, most of these intermediate steps are taken care by the authentication libraries which are already written and provided by the sdk specific to whatever login providers we're gonna implement for. Hence, we'd simply install the necessary libraries for the respective providers and pass-in necessary configuration information for these libraries to work. In ASP.NET Core, we make use of Authentication Middleware and Cookies which handle the authentication steps and set auth cookie for the authenticated user in the browser window.

Getting into HandsOn - Creating Login Module:

Let's get started with our implementation, by creating a new webapp project using the dotnetcore command-line.


> dotnet new mvc --name OidcApp

This creates all the boilerplate code for our application so that we can have a headstart. Please note that we'd be using dotnetcore3.1 for our development and so all the necessary packages would be used in-line to the base sdk. But this configuration still works for dotnetcore2.2 as well, since the logic remains same.

Once the installation completes, let's get into the project and start building a few prerequisites before heading into social login. The usecase is that for all logging in users, we shall check if the user is already registered into the application and if not already registered we'd create a new user entity out of the profile information we receive from the login provider and store the object in the database. This helps us in integrating our login module which we're developing with other functional modules in the application which might require a registered user. For the database layer, we shall use EF Core which simplifies the domain integration and lets us focus on the functionality. On top of EF Core, we shall use a UserRepository which encapsulates domain logic for handling user entity.

Setting up DbContext and User Repositories

To setup EF Core, we shall install the below packages, which help in connecting and creating a bridge between the application and the database.


> dotnet package add Microsoft.EntityFrameworkCore.Design

> dotnet package add Microsoft.EntityFrameworkCore.SqlServer

We'd also setup a dbcontext class which is used for database scaffolding of the entities. And then register the dbcontext as a service for injecting into the requesting classes.


/* dbcontext class */

using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;

namespace OidcApp.Models.Entities
{
    public class AppDbContext : DbContext
    {
        public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
        {
        }
        public DbSet<UserProfile> UserProfiles { get; set; }
    }

    public class UserProfile
    {
        [Key]
        public int Id { get; set; }
        public string EmailAddress { get; set; }
        public string OIdProvider { get; set; }
        public string OId { get; set; }
    }
}

/*Startup class - ConfigureServices() method */

services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
});

Learn more about how to setup EF Core in ASP.NET Core application.

This dbcontext is used in the UserRepository class which takes care of handling user entity within the database. A Repository class encapsulates the domain logic for a given responsibility and lets the other components request for functionality by means of an injected abstraction.

We'd create a user repository in the same lines for its responsibility in this case is User entity, as below:


using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using OidcApp.Models.Entities;
using OidcApp.Models.Providers;

namespace OidcApp.Models.Repositories
{
    public interface IUserRepo
    {
        Task<bool> GetOrCreateExternalUserAsync(UserProfile id, HttpContext httpContext);
    }

    public class UserRepo : IUserRepo
    {
        private readonly AppDbContext context;
        private readonly IUserManager userManager;

        public UserRepo(AppDbContext context, IUserManager userManager)
        {
            this.context = context;
            this.userManager = userManager;
        }

        public async Task<bool> GetOrCreateExternalUserAsync(UserProfile id, HttpContext httpContext)
        {
            if (id != null)
            {
                UserProfile user = context.UserProfiles.FirstOrDefault(x => x.OId == id.OId && x.OIdProvider == id.OIdProvider);

                if (user == null)
                {
                    user = id;
                    await context.UserProfiles.AddAsync(user);
                    await context.SaveChangesAsync();
                }
                
                // optionally call userManager.SignIn() 
                // to setup additional claims apart from the ones
                // received from the social login
                // await userManager.SignIn(httpContext, user);
                
                return true;
            }
            
            return false;
        }
    }
}

Learn more about using Repository classes with EF Core in ASP.NET Core application

Observe that we're injecting database context class via constructor along with another dependency IUserManager which takes the responsibility of handling user identity, we shall setup in the next step.

Setting up UserManager for Customized User Identity

The UserManager class takes care of the user identity which is setup once a user login is authenticated for a given login provider. This is purely optional but can be necessary when we'd need to customize the already available User Identity by adding more claims respective to the application functionality (such as adding claims to represent an administrator or any other specific information out of database) which can later be used for authorizing user for accessing features or modules. We can make use of these added claims onto an Authorization Policy for this purpose.

In our case, the UserManager class exposes two methods; one for Signing In and other for Signing out as below:


using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using OidcApp.Models.Entities;

namespace OidcApp.Models.Providers
{
    public interface IUserManager
    {
        Task SignIn(HttpContext httpContext, UserProfile user, bool isPersistent = false);
        Task SignOut(HttpContext httpContext);
    }

    public class UserManager : IUserManager
    {
        public async Task SignIn(HttpContext httpContext, UserProfile user, bool isPersistent = false)
        {
            string authenticationScheme = SocialAuthenticationDefaults.AuthenticationScheme;

            // Generate Claims from DbEntity
            var claims = GetUserClaims(user);
            
            // Add Additional Claims from the Context
            // which might be useful
            claims.Add(httpContext.User.Claims.FirstOrDefault(x => x.Type == ClaimTypes.Name));
            
            ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, authenticationScheme);
            ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            
            var authProperties = new AuthenticationProperties
            {
                // AllowRefresh = <bool>,
                // Refreshing the authentication session should be allowed.
                // ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(10),
                // The time at which the authentication ticket expires. A 
                // value set here overrides the ExpireTimeSpan option of 
                // CookieAuthenticationOptions set with AddCookie.
                // IsPersistent = true,
                // Whether the authentication session is persisted across 
                // multiple requests. Required when setting the 
                // ExpireTimeSpan option of CookieAuthenticationOptions 
                // set with AddCookie. Also required when setting 
                // ExpiresUtc.
                // IssuedUtc = <DateTimeOffset>,
                // The time at which the authentication ticket was issued.
                // RedirectUri = <string>
                // The full path or absolute URI to be used as an http 
                // redirect response value.
            };

            await httpContext.SignInAsync(authenticationScheme, claimsPrincipal, authProperties);
        }

        public async Task SignOut(HttpContext httpContext)
        {
            await httpContext.SignOutAsync(SocialAuthenticationDefaults.AuthenticationScheme);
        }

        private List<Claim> GetUserClaims(UserProfile user)
        {
            List<Claim> claims = new List<Claim>();
            claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            claims.Add(new Claim("Provider", user.OIdProvider));
            claims.Add(new Claim(ClaimTypes.Email, user.EmailAddress));
            return claims;
        }
    }
}

This UserManager which otherwise is a logical class is converted into Service by extracting an abstraction IUserManager for the concrete class UserManager and by injecting the abstraction wherever needed. In our case we had already injected this service within the Repository class. This kind of design enforces and satisfies the Interface Segregation and Dependency Inversion principles, which help in better maintained application instances for us.

Setting up Controllers and Login Callback

Finally, we'd need to setup the face for all this logic we've built in our application: the endpoints.

We'd need two endpoints for our cause -

  • for SignIn and 
    
  • for Signout. 
    

Within the Signin functionality we'd have two endpoints -

  • one which the user clicks to initiate a redirect to the specific provider and
    
  • one which is invoked as a callback from the identity provider once the user is authenticated
    

For this, we'd add a new Controller class called UserController, which exposes the endpoints as mentioned above. We'd try to be as generic as possible for our cause, which can help us in extending the same logic for as many providers as possible without having to modify the code.


/* UserController.ExternalLogin() which handles the redirect to provider */
[HttpGet, Route("[controller]/ExternalLogin")]
public IActionResult ExternalLogin(string returnUrl, string provider = "google")
{
    string authenticationScheme = string.Empty;

    // Logic to select the authenticationScheme
    // which specifies which LoginProvider to use
    // comes in here

    var auth = new AuthenticationProperties
    {
        RedirectUri = Url.Action(nameof(LoginCallback), new { provider, returnUrl })
    };

    return new ChallengeResult(authenticationScheme, auth);
}

This endpoint /user/externallogin is tied up to a button in the View for a given login provider and a returnUrl, on click of which we'll return a ChallengeResult() which invokes the Authentication middleware for that specific login provider.

Since most of the login providers use OAuth as the protocol for authentication, creating a generic authentication configuration becomes easy for us. Most of the login providers require two things to be configured to authenticate a user against a registered application

  • ClientId
  • ClientSecret

This approach is same for providers such as Google, Facebook which first require us to create and register an application in their developer consoles after which we can obtain these values. In our application, we shall maintain these values in our appsettings.json as below format:


"Oidc": {
    "providers": [
      {
        "name": "facebook",
        "clientId": "xxxxxxxxxxxxx",
        "clientSecret": "xxxxxxxxxxxxxxx",
        "redirectUrl": "/user/me"
      },
      {
        "name": "google",
        "clientId": "xxxxxxxxxxxxx-xxxxxxxxxxxxxxx.apps.googleusercontent.com",
        "clientSecret": "xxxxxxxxxxxxx",
        "redirectUrl": "/user/me"
      }
    ]
  }

And use these configurations to bind to respective login providers for their authentication middlewares. Next, let's integrate the Login module we have created so far with two most popular social login providers and look at how we can provide users with an elegant login experience, Google and Facebook.

Integrating with Facebook

Integrating with Google