Card image cap

Configuring Database and Social Login Providers in IdentityServer4

IdentityServer4  • Posted one month ago

So far we have seen how IdentityServer4 helps developers separate authentication layer from the actual application facilitating the applications to focus only on their business logic while the user authentication and session management is handled by the IdentityServer4 securing the application endpoints from unwanted access.

we have also looked at setting up a simple SecureTokenServer using IdentityServer4 library and the various authentication flows prescribed by the OAuth standard for securing APIs and issuing tokens under various usecases and scenarios.

In this article, let's look at all that is remaining - connecting to an actual Database for Client configurations and User management. This is important because you can't hold all that user and configuration data in-memory inside the TokenServer - its both tedious and dangerous. You can't change any configuration data without stopping the server, which basically doesn't work at all for applications with production-scale loads in mind.

We shall also look at one more section - adding social logins! offering social logins is very important and advantageous these days where the chance of using a social login is higher than a normal email-based login when you look at the current stats.

Goal 1 - Adding Database and Configuration Store:

Connecting to database is no big headache in IdentityServer4, if you ask me. Because it provides us with the necessary DatabaseContext classes ready to use which work on EntityFrameworkCore. We just need to register these DatabaseContexts in our TokenServer and let the IdentityServer4 take care of the tables creation for us.

With regards to User management, IdentityServer4 handles this by means of AspNetIdentity framework, which provides with a robust and insightful user management libraries for use.

Let's get started with configuring DatabaseContexts for Clients and Resource data, which doesn't require much change in our existing TokenServer application. We shall begin by adding the below packages in our application:

<PackageReference Include="IdentityServer4.EntityFramework" Version="4.0.4" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="3.1.7" />
<PackageReference Include="Microsoft.AspNetCore.Identity.UI" Version="3.1.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.7">
    <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    <PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.7" />

We're basically adding the necessary packages for working with EntityFrameworkCore, along with them we're also adding the IdentityServer4.EntityFramework package which adds the required DatabaseContexts for Client and Resource stores.

Next, we need to register the respective DatabaseContexts in our application:

var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

// IdentityResources are Constants Mostly
// OpenId, Profile, Email and so on
builder.AddInMemoryIdentityResources(Config.IdentityResources);

// Adds Clients, ApiResources and Cors
// from Database
builder.AddConfigurationStore(options =>
{
    options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
        sql => sql.MigrationsAssembly(migrationsAssembly));
});

// Adds Grants from Database
builder.AddOperationalStore(options =>
{
    options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
        sql => sql.MigrationsAssembly(migrationsAssembly));
});

The AddConfigurationStore() method adds the necessary Database Tables for storing the Client, ApiResource data which the AddOperationalStore() method adds the necessary Tables for storing the Grant information of these Clients. Both these Stores are linked to the IdentityServer we're setting up inside our TokenServer.

var builder = services.AddIdentityServer(options =>
{
    options.Events.RaiseErrorEvents = true;
    options.Events.RaiseInformationEvents = true;
    options.Events.RaiseFailureEvents = true;
    options.Events.RaiseSuccessEvents = true;
    options.EmitStaticAudienceClaim = true;
});

Once these are added, we need to run a migration so that these contexts are picked up and the tables are created in the database we're pointing to.

> dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/PersistedGrantDb
> dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/ConfigurationDb

These commands complete our configuration for Clients. Next, we can add some Clients to our "ClientStore" which the IdentityServer now picks up for its functioning during runtime.

var context = serviceScope.ServiceProvider
    .GetRequiredService<ConfigurationDbContext>();

if (!context.Clients.Any())
{
    foreach (var client in Config.Clients)
    {
        context.Clients.Add(client.ToEntity());
    }

    context.SaveChanges();
}
if (!context.IdentityResources.Any())
{
    foreach (var resource in Config.IdentityResources)
    {
        context.IdentityResources.Add(resource.ToEntity());
    }
    context.SaveChanges();
}

if (!context.ApiScopes.Any())
{
    foreach (var apiScope in Config.ApiScopes)
    {
        context.ApiScopes.Add(apiScope.ToEntity());
    }
}

if (!context.ApiResources.Any())
{
    foreach (var resource in Config.ApiResources)
    {
        context.ApiResources.Add(resource.ToEntity());
    }
    context.SaveChanges();
}

We're reading the Client, ApiResources and Scopes data from the Config class we've been using so far and seeding the database.

public static IEnumerable<Client> Clients =>
    new Client[]
    {
        new Client
        {
            ClientId = "client",
            AllowedGrantTypes = GrantTypes.ClientCredentials,
            ClientSecrets =
            {
                new Secret("secret".Sha256())
            },
            AllowedScopes = { "api1" }
        },
        new Client {
            ClientId = "angular_spa",
            ClientName = "Angular 4 Client",
            AllowedGrantTypes = GrantTypes.Implicit,
            AllowedScopes = new List<string> { 
                "openid", "profile", "api1", "email" },
            RedirectUris = new List<string> { 
                "http://localhost:4200/auth/auth-callback?id=1234" },
            PostLogoutRedirectUris = new List<string> { 
                "http://localhost:4200/" },
            AllowedCorsOrigins = new List<string> { 
                "http://localhost:4200" },
            AllowAccessTokensViaBrowser = true
        },
        new Client {
            ClientId = "pkce_client",
            ClientName = "PKCE Client",
            AllowedGrantTypes = GrantTypes.Code,
            AllowedScopes = new List<string> { 
                "openid", "profile", "api1", "email" },
            RedirectUris = new List<string> { 
                "http://localhost:4200/auth/auth-callback?id=1234" },
            PostLogoutRedirectUris = new List<string> { 
                "http://localhost:4200/" },
            AllowedCorsOrigins = new List<string> { 
                "http://localhost:4200" },
            AllowAccessTokensViaBrowser = true
        }
     };

When run the TokenServer with this setup, we're still able to use our Client configurations with the connected client applications while we're able to modify them on the fly from the database without having to touch the TokenServer in any way.

Goal 2 - Adding UserStore:

While adding ConfigurationStore didn't need any change in our TokenServer logic, adding a UserStore needs little changes in the Views and Controllers so far we're using for our User Interactive Clients using the Authorization_Code and Implicit Grants we discussed before. To make things simple, we have another is4 template which does this for us.

Let's create a new project elsewhere from this template and pickup things we're interested in.

> dotnet new is4aspid --name test

This creates an ASP.NET Core project with IdentityServer4 installed along with UserIdentity Controllers and Views. Let's just copy up the Controllers and Views part alone (we'd do the Startup changes by ourselves).

Once copied, we'd have so many missing pieces as echoed by the compilation errors we get. To add the Identity support, let's add the below package into our project along side the previously installed packages.

<PackageReference Include="IdentityServer4.AspNetIdentity" Version="4.0.4" />

This should solve the compilation errors as much as possible, if not try checking for the namespace issues (we literally lifted the code from another project right? hahaha.)

In the Startup class, add the below things to link up the IdentityServer to use UserIdentity as the UserStore.

// Adding DatabaseContext for use in application
services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

---- code for adding ConfigurationStore ----
---- IdentityServerBuilder goes here ----

// Adds IdentityUsers as UserStore from Database
builder.AddAspNetIdentity<ApplicationUser>();

That's all. We're now good to use our UserStore powered by AspNetIdentity to be used for storing user credentials and information by the IdentityServer whenever a new user registers or tries to login to the TokenServer.

To seed our UserStore with some intial set of Users for testing our TokenServer, we can do something like below:

var identityContext = serviceScope.ServiceProvider
    .GetService<ApplicationDbContext>();

if (!identityContext.Users.Any())
{
    var userMgr = serviceScope.ServiceProvider
        .GetRequiredService<UserManager<ApplicationUser>>();

    var alice = userMgr.FindByNameAsync("alice").Result;

    if (alice == null)
    {
        alice = new ApplicationUser
        {
            UserName = "alice",
            Email = "AliceSmith@email.com",
            EmailConfirmed = true,
        };
        
        var result = userMgr.CreateAsync(alice, "Pass123$").Result;
        if (!result.Succeeded)
        {
            throw new Exception(result.Errors.First().Description);
        }

        result = userMgr.AddClaimsAsync(alice, new Claim[]{
            new Claim(JwtClaimTypes.Name, "Alice Smith"),
            new Claim(JwtClaimTypes.GivenName, "Alice"),
            new Claim(JwtClaimTypes.FamilyName, "Smith"),
            new Claim(JwtClaimTypes.WebSite, "http://alice.com"),
        }).Result;
        
        if (!result.Succeeded)
        {
            throw new Exception(result.Errors.First().Description);
        }
        
        Log.Debug("alice created");
    }
    else
    {
        Log.Debug("alice already exists");
    }
}

Goal 3 - Adding Social Logins:

We're done with our Database exercise. Let's finish this up by adding a few social login options as well. Let's add Google and Facebook social logins to our TokenServer so that when a certain application redirects to the TokenServer for User authentication, the user can choose from either of the three (EMail, Google or Facebook) for authentication.

It's a quite simple affair. We just need to add the necessary Authentication libraries for the Social Login providers we're interested in and then hook them up to the Authentication middleware.

<PackageReference Include="Microsoft.AspNetCore.Authentication.Facebook" Version="3.1.7" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Google" Version="3.1.7" />

And then in the Startup class:

var connectionString = Configuration.GetConnectionString("DefaultConnection");

var providers = new List<ExternalProvider>();
Configuration.Bind("ExternalProviders", providers);
services.AddSingleton(new Providers { ExternalProviders = providers });

--- IdentityServer configurations ---

var builder = services.AddAuthentication();
foreach (var provider in providers)
{
    switch (provider.AuthenticationScheme.ToLowerInvariant())
    {
        case "google":
            builder.AddGoogle(
                    provider.AuthenticationScheme, 
                    provider.DisplayName, 
                    options => {
                        options.ClientId = provider.ClientId;
                        options.ClientSecret = provider.ClientSecret;
                        options.SignInScheme = 
                            IdentityServerConstants.ExternalCookieAuthenticationScheme;
            });
        break;

        case "facebook":
            builder.AddFacebook(
                    provider.AuthenticationScheme, 
                    provider.DisplayName, 
                    options => {
                        options.ClientId = provider.ClientId;
                        options.ClientSecret = provider.ClientSecret;
                        options.SignInScheme = 
                            IdentityServerConstants.ExternalCookieAuthenticationScheme;
            });
        break;
    }
}

I'm just trying to put a bit of externalization here in place by pushing the provider, clientId, clientSecret and few other things from the appsettings.json and then configuring the services automatically based on the settings I configured.

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(LocalDb)\\MSSQLLocalDB;database=Ids4-Sts;trusted_connection=yes;"
  },
  "ExternalProviders": [
    {
      "AuthenticationScheme": "Google",
      "DisplayName": "Signin with Google",
      "ClientId": "abcd",
      "ClientSecret": "abcd",
      "ButtonStyle": "btn-danger"
    },
    {
      "AuthenticationScheme": "Facebook",
      "DisplayName": "Signin with Facebook",
      "ClientId": "abcd",
      "ClientSecret": "abcd",
      "ButtonStyle": "btn-info"
    }
  ]
}

I'm not covering the stuffs regarding how to fetch these clientId and clientSecret credentials from Google and Facebook, since its the same process as the one we've done while adding social logins to an ASP.NET Core application.

Finally, when we run this setup, what I get for my Login page is this:

data/Admin/2020/9/login-ids4.png

*I just tweaked the Login page a bit, so it looks a lot better now (atleast I feel so).

Final Thoughts:

IdentityServer4 helps developers spend less time on their user management and authentication components by providing a highly customizable and performant library which helps anyone build a TokenServer for their application at ease. By adding a backend datastore to the mix, we can not only configure new Clients when required, but also easily troubleshoot and maintain our user data at ease.

The complete source code of the application we've discussed in the article, together all the things assembled for a minimalistic TokenServer is available at the below repository https://github.com/referbruv/simple-sts (Do give a Star if you find the solution useful).

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