Identityserver4 Asp.net Core e EntityFramework - Parte 1

IdentityServer

Ecco uno step by step per implementare un'architettura abbastanza frequente contenente un Identityserver4 per quanto riguarda tutti gli aspetti relativi all'autenticazione e all'autorizzazione, un sito web MVC realizzato con Asp.net Core e un'applicazione Web API che fornisce i dati al sito (o eventualmente anche a altri client quali Device App). Lo schema viene illustrato di seguito

Come vediamo dall'immagine gli utenti fanno accesso al Sito web utilizzando le credenziali memorizzate all'interno del DB Identity Data. Quando nasce l'esigenza di un accesso ai dati dell'applicazione il sito web agisce come un Client (registrato sul DB Identity Data) che interroga il servizio Rest implementato dal Web Api Core site. Affinchè la chiamata al servizio vada a buon fine il Web Site deve presentare un Token access contenente l'autorizzazione all'accesso al servizio.

Cominciamo con la creazione dell'IdentityServer4. Ci soffermeremo all'inizio ovviamente sulla classe Startup.cs. Nel nostro caso siamo partiti da un progetto preimpostato che troverete come progetto GitHub sotto la cartella Quickstarts. Siccome si è deciso di utilizzare EntityFramework con ApsnetIdentity per mantenere le informazioni relative ai dati di Identity, siamo partiti dal progetto Quickstart8, ma il consiglio è di partire con un progetto nuovo di tipo Asp,Net Core con Authentication 'None' e aggiungere pezzo per pezzo i componenti che servono.

Dopo aver creato il progetto IdentityServer4 di tipo Asp.Net core sarà necessario fornire i pacchetti per la gestione IdentityServer4, EntityFramework e AspnetIdentity. Di seguito i pacchetti da installare tramite nuget:

Adesso si devono effettuare effettuare le operazioni preliminari per la creazione del DataBase per Identity Server. L'approccio è un approccio EntityFramework (Core) Code First. Grazie quindi alle cosiddette migrazioni, partendo dai DbContext si producono dei modelli sulla base dei quali si possono costruire successivamente i relativi DataBase. I DbContext utilizzati sono 3:

  • ApplicationDbContext (Tabelle per la gestione di Aspnet Identity - Users, roles, claims)
  • ConfigurationDbContext (Tabelle di gestione di clients, resources, codes, tokens, consents)
  • PersistedGrantDbContext (Tabelle di persistenza dei Tokens)

Il primo DbContext è molto semplice come vediamo di seguito:

 public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
    {

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        { }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
            // Customize the ASP.NET Identity model and override the defaults if needed.
            // For example, you can rename the ASP.NET Identity table names and more.
            // Add your customizations after calling base.OnModelCreating(builder);
        }
    }

Eredita dalla classe IdentityDbContext che fa riferimento ad una ApplicationUser definita così:

 public class ApplicationUser : IdentityUser
    {
    }

Questo ci permette di inserire eventuali altre proprietà custom alla classe IdentityUser da cui eredita. Gli altri due Context sono definiti all'interno della classe StartUp che vedremo nel successivo paragrafo.

StartUp

Veniamo adesso alla classe StartUp dove si implementano tutte le varie funzionalità necessarie all'autenticazione e all'autorizzazione. Ecco il codice:

 public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
        
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            const string connectionString = @"Data Source=.\MSSQLLocalDB;database=IdentityDB;trusted_connection=yes;";
            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;

            // configure identity server store, keys, clients and scopes
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(connectionString));

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

            var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;
            })
               .AddAspNetIdentity<ApplicationUser>()
               // this adds the config data from DB (clients, resources)
               .AddConfigurationStore(options =>
               {
                   options.ConfigureDbContext = b =>
                       b.UseSqlServer(connectionString,
                           sql => sql.MigrationsAssembly(migrationsAssembly));
               })
               // this adds the operational data from DB (codes, tokens, consents)
               .AddOperationalStore(options =>
               {
                   options.ConfigureDbContext = b =>
                       b.UseSqlServer(connectionString,
                           sql => sql.MigrationsAssembly(migrationsAssembly));

                    // this enables automatic token cleanup. this is optional.
                    options.EnableTokenCleanup = true;
                   options.TokenCleanupInterval = 30; // frequency in seconds to cleanup stale grants. 15 is useful during debugging
                });
        }

Dopo aver aggiunto il middleware di MVC, indichiamo la connectionstring che servirà ad individuare il Database (che ancora non abbiamo creato) che conterrà le tabelle necessarie. Poi si registra lo specifico context come servizio di Identity indicando la connectionstring creata prima. Analogamente si aggiunge il sistema di Identity di default  per gli specifici Users e Roles ed infine si configura Identityserver affinchè utilizzi le implementazioni di Asp,Net Identity per quanto riguarda IUserClaimsPrincipalFactory,
IResourceOwnerPasswordValidator e IProfileService. Come ultima operazione si configura l'implementazione di EF implementation per  IClientStore, IResourceStore e ICorsPolicyService (per le chiamate Cross Domain ai servizi API).

Adesso possiamo creare i modelli usando le Migrazioni da riga di comando e, una volta create le migrazioni, si crea il database tramite il comando (sempre dentro la riga di comando) Update Database. La riga di comando è quella di Packet Manager Console come di seguito illustrato:

Il risultato nel progetto è qualcosa di molto simile a questo:

Come vediamo tutti i nomi delle migrazioni hanno come prefisso il timestamp di creazione. Il Database creato ha questo tipo di formato:

A questo punto non rimane che aggiungere un Controller con la action Index e costruire una view che punta alla descrizione degli endpoint del server

Cliccando su discovery document otteniamo la lista:

Il codice definitivo lo troverete al nostro repository Git