How to create Social Logins with ASP.NET Core

This article is a brief intro about how to create Social Logins with ASP.NET Core. We will design a simple flow and follow it up with Google Facebook logins.

Introduction

Social Logins have become a convenient and efficient approach in web app logins and has become one of the most preferred login methods by the users.

Popular social networks such as Google, Facebook, LinkedIn, Twitter and others offer secure and unified mechanisms to validate user identities for relying parties.

Users who have these accounts can login into the web applications using their respective social login credentials.

Web Applications can then authenticate with these login providers and authorize the requesting web application access to their profile data already created in these login providers.

In this article, 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.

We will look at how we can design an application and integrate with Facebook and Google Logins.

How Social Logins work?

Most of the social authentication providers implement authentication by means of OAuth2 protocol.

OAuth2 or Open Authentication 2 is an open source 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.

Social Login Authentication Flow

For example, let’s say a user wants to use an application myninja.com that requires the user to login with any of the social login options supported by the application. Let’s say the user decides to use his Google credentials for login to myninja.com.

The authentication flow is as follows –

  1. User clicks on a login button in myninja.com and clicks on “Login with Google”
  2. myninja.com redirects user to Google.com and user enters the credentials for Google
  3. Upon successful login, Google.com redirects back to myninja.com and passes an authorization code
  4. myninja.com requests Google.com for necessary details about the User in exchange for the authorization code.

I have explained about the OAuth2 protocol and differences with OpenID and JWT in detail in this article. Please check it out if you want to learn more about the topic.

Authentication Middlewares in .NET

In ASP.NET Core, most of these intermediate steps are taken care of by the authentication middleware libraries which are already written and provided by the sdk specific to whatever login providers we’re gonna implement for.

To implement a specific login authentication, we can simply install the necessary libraries for the respective providers and pass-in necessary configuration information for these libraries to work.

We use Authentication Middleware and Cookies which handle the authentication steps and set auth cookies for the authenticated user in the browser window.

How to get started with Social Logins with ASP.NET Core

Let’s get started with our implementation, by creating a new web app project using the Dotnet Core CLI.

dotnet new mvc --name OidcApp

Once the project is restored, let’s get into the project and start building a few prerequisites before heading into social login.

For all logging in users, we will check if the user is already registered into the application and if not already registered we 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 entities.

Setting up DbContext and User Repositories

To set up 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 will also set up 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.

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; }
    }
}

We will register DbContext in Startup.cs as below –

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

I have explained in-detail about how to set up EF Core in ASP.NET Core application in this article. Please take a look if you want to learn more.

The DbContext is used in the UserRepository class which takes care of handling user entities 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 create a user repository in the same lines for its responsibility in this case is User entity, as below.

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 set up additional claims apart from the ones
                // received from the social login
                // await userManager.SignIn(httpContext, user);


                return true;
            }


            return false;
        }
    }
}

You can learn more about the importance of using Repository pattern and how to use it with EF Core in ASP.NET Core application in this detailed article.

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 set up 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 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 the database) which can later be used for authorizing users for accessing features or modules.

We can use these added claims on 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.

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;
        }
    }
}

The concrete class UserManager implements the interface IUserManager. We register these two as a service for resolution in the Dependency Injection container.

Setting up Controllers and Login Callback

We need to set up the face for all this logic we’ve built in our application: the endpoints.

We’d need two endpoints for signing In users into the web application and signing users out.

Within the Signin functionality we have two endpoints –

  • The endpoint that is called when user clicks, to initiate a redirect to the specific provider
  • The endpoint that is invoked as a callback from the identity provider once the user is authenticated

For this, we 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.

public class UserController : Controller
{
    /* 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.

Obtaining ClientID and ClientSecret from Identity Providers

Most of the login providers use OAuth as the protocol for authentication. These require two things to be configured to authenticate a user against a registered application.

  • ClientId
  • ClientSecret

This approach is the 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.

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

Add these configurations to bind to respective login providers for their authentication middlewares.

Conclusion – Integrating with Google and Facebook

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.

In these articles, I explain in-detail about how to create and obtain ClientID and ClientSecret from Google and Facebook, how to configure Authentication Middleware and How to test for functionalities.

I hope you find these informative and educational.

I have also created an efficient boilerplate solution that has solved implementations of Google and Facebook logins – SocialNinja. Please do check it out and leave a star!


Buy Me A Coffee

Found this article helpful? Please consider supporting!

Ram
Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity. You can connect with me on Medium, Twitter or LinkedIn.

Leave a Reply

Your email address will not be published. Required fields are marked *