Pages

Monday, May 8, 2017

IdentityServer3 with PKCE Part 4 - Persisting User Data

This series simulates a native application accessing a protected Web API resource, using OAuth2 via IdentityServer3. It demonstrates using Proof Key for Code Exchange (PKCE), and is in four parts:

  1. Build a simple authorization server, consumed by native application.
  2. Build a protected resource.
  3. Persist server configuration to database.
  4. Persist user data to database using Microsoft.Identity and SQL Server.

Important
This series does not create an OpenID Connect (OIDC) server. It is OAuth-only, since the PKCE specification doesn't require OIDC.

Where We're At

We're persisting IdentityServer configuration data, but still using in-memory users. IdentityServer can be extended to validate against an identity store, and supports Microsoft's AspNet.Identity via Entity Framework on SQL Server.

Important IdentityServer is not intended or used to add, edit or delete users. They make that clear. The server's job is only to verify identity. In this sample, there's a klunky method for populating the user database, and there are no API methods for registering a user. The API could be added using controller code similar to what's in the default ASP.NET Web API templates.

AspNetIdentity

References
IdentityServer3.AspNetIdentity Sample
Identity Server 3 using ASP.NET Identity
OAuth2 Walkthrough using Identity Server and ASP.NET

Install these packages.

Install-Package IdentityServer3.AspNetIdentity -Project AuthAndApi
Install-Package Microsoft.AspNet.Identity.EntityFramework -Version 2.2.1 -Project AuthAndApi

Add a new connection string to web.config, for the AspNet Identity database.

    <add name="IdUsersDb" connectionString="Server=(localDb)\MSSQLLocalDb;AttachDbFilename=|DataDirectory|IdUsers.mdb;Database=IdUsers-ncacpkce;Integrated Security=true;" providerName="System.Data.SqlClient" />

Update Factory.cs

//Add these usings
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.AspNet.Identity;
using IdentityServer3.AspNetIdentity;
using IdentityServer3.Core.Services;
using IdentityServer3.Core.Services.InMemory;

Change the Configure method, adding code to persist the users, and a new method to populate the database.

        public static IdentityServerServiceFactory Configure(string optionsConnectionString, string usersConnectionString)
        {
            //Configure persisting IdentityServer operational services
            //The database will be created if needed. The default schema is being used.
            var efConfig = new EntityFrameworkServiceOptions
            {
                ConnectionString = optionsConnectionString
            };

            // these two calls just pre-populate the test DB from the in-memory config.
            // clf note: there's probably a better way to do this for a production app.
            ConfigureClients(Clients.Get(), efConfig);
            ConfigureScopes(Scopes.Get(), efConfig);

            var factory = new IdentityServerServiceFactory();
            factory.RegisterConfigurationServices(efConfig);
            factory.RegisterOperationalServices(efConfig);
            factory.ConfigureClientStoreCache();
            factory.ConfigureScopeStoreCache();
            //persist users
            //populate the database. This creates the database, if needed.
            ConfigureUsers(Users.Get(), usersConnectionString);
            //configure IdentityServer to use the database
            factory.UserService = new Registration<IUserService>(UserServiceFactory.Create(usersConnectionString));

            //configure cleanup for expired data
            var cleanup = new TokenCleanup(efConfig); //by default every 60 seconds

            return factory;
        }

        public static void ConfigureUsers(IEnumerable<InMemoryUser> users, string connectionString)
        {
            //create the Identity UserManager
            var context = new IdentityDbContext(connectionString);
            var userStore = new UserStore<IdentityUser>(context);
            var userManager = new UserManager<IdentityUser>(userStore);
            foreach (var user in users)
            {
                //no error checking!
                //only add if doesn't exist
                var test = userManager.FindByName(user.Username);
                if (test != null) { continue; }
                userManager.Create(new IdentityUser(user.Username), user.Password);
                test = userManager.FindByName(user.Username);
                foreach (var claim in user.Claims)
                {
                    if (claim.Type.ToLower() == "email") { test.Email = claim.Value; }
                    userManager.AddClaim(test.Id, claim);
                }
                userManager.Update(test);
            }
        }

And finally, add a new factory class, which is used in the line factory.UserServce =.

   public static class UserServiceFactory
    {
        public static AspNetIdentityUserService<IdentityUser, string> Create(string connectionString)
        {
            var context = new IdentityDbContext(connectionString);
            var userStore = new UserStore<IdentityUser>(context);
            var userManager = new UserManager<IdentityUser>(userStore);
            return new AspNetIdentityUserService<IdentityUser, string>(userManager);
        }
    }

That's it! The Startup class doesn't change. Running the solution creates the new database, and the authentication works as before.

2017-05-06_165055

Wrap Up

In this series, I demonstrated authorizing a mobile application in a secure way. This was necessarily a simple sample. Some natural next steps and questions to answer would be:

  1. How to customize the IdentityServer login and authorization screens
  2. Configure for OpenID Connect3
  3. Show using Google authentication

No comments:

Post a Comment