Glenn singh
Glenn singh

Reputation: 263

.NET Core Api authentication using Identity server 4

I am trying to create a small ecommerce demo app using .net core API 3.1 with Identity server 4.

Project Structure


Config.cs (Demo.Auth Project)

    public static class Config
    {
        public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            {                
                new IdentityResources.Profile(),
            };
        public static IEnumerable<ApiResource> ApiResources => new[]
        {
            new ApiResource("Demo.Api", "Demo Api")
        };

        public static IEnumerable<Client> Clients => new[]
        {
            new Client()
            {
                ClientId = "mvc",
                ClientName = "Demo.MvcClient",
                AllowedGrantTypes = GrantTypes.ClientCredentials,
                RequirePkce = true,
                ClientSecrets =
                {
                    new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256())
                },
                RedirectUris = {"http://localhost:5003/signin-oidc"},
                FrontChannelLogoutUri = "http://localhost:5003/signout-oidc",
                PostLogoutRedirectUris = {"http://localhost:5003/signout-callback-oidc"},

                AllowOfflineAccess = true,
                AllowedScopes = {"profile"}
            }
        };
    }


Startup.cs (Demo.Auth Project)

    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();
            IConfigurationRoot config = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json")
                .Build();

            string identityConnectionString = config.GetSection("ConnectionStrings")
                .Value;
            var migratingAssembly = typeof(Startup).GetTypeInfo()
                .Assembly.GetName()
                .Name;

            if (config.GetValue<bool>("UseInMemoryDatabase"))
            {
                services.AddIdentityServer(options =>
                    {
                        options.Events.RaiseErrorEvents = true;
                        options.Events.RaiseInformationEvents = true;
                        options.Events.RaiseFailureEvents = true;
                        options.Events.RaiseSuccessEvents = true;
                    })
                    .AddTestUsers(TestUsers.Users)
                    .AddInMemoryIdentityResources(Config.Ids)
                    .AddInMemoryApiResources(Config.ApiResources)
                    .AddInMemoryClients(Config.Clients)
                    .AddDeveloperSigningCredential();
            }
            else
            {
                services.AddIdentityServer(options =>
                    {
                        options.Events.RaiseErrorEvents = true;
                        options.Events.RaiseInformationEvents = true;
                        options.Events.RaiseFailureEvents = true;
                        options.Events.RaiseSuccessEvents = true;
                    })
                    .AddTestUsers(TestUsers.Users)
                    .AddDeveloperSigningCredential()
                    //This will store client and ApiResource
                    .AddConfigurationStore(options =>
                    {
                        options.ConfigureDbContext = b => b.UseSqlServer(identityConnectionString,
                            sql => sql.MigrationsAssembly(migratingAssembly));
                    })
                    //This will store token, consent or code
                    .AddOperationalStore(options =>
                    {
                        options.ConfigureDbContext = b => b.UseSqlServer(identityConnectionString,
                            sql => sql.MigrationsAssembly(migratingAssembly));
                    });
            }
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app,
            IWebHostEnvironment env)
        {
            // this will do the initial DB population
           // InitializeDatabase(app);

            if (env.IsDevelopment())
                app.UseDeveloperExceptionPage();

            app.UseRouting();
            app.UseIdentityServer();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/",
                    async context => { await context.Response.WriteAsync("Hello World!"); });
            });
        }       
    }


Startup.cs (API Project)

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication("Bearer").AddIdentityServerAuthentication(options =>
            {
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;
                options.ApiName = "Demo.Api";
            });

            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment()) app.UseDeveloperExceptionPage();

            app.UseRouting();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }
    }


WeatherForecastController (of Demo.Api project)

    [Authorize]
    [ApiController]
    [Route("[controller]")]
    public class WeatherForecastController : ControllerBase
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private readonly ILogger<WeatherForecastController> _logger;
        public WeatherForecastController(ILogger<WeatherForecastController> logger)
        {
            _logger = logger;
        }

        [HttpGet]
        public IEnumerable<WeatherForecast> Get()
        {
            var rng = new Random();
            return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
        }
    }


I test the API in postman and it is working fine. "Demo.Auth" project is generating the token and I am able to access my authorize controller successfully.

Idea here is:

MVC Client ----> Identity Server Project ---> API

MVC client wants to access the API. So I will authenticate the Mvc client on Identity server project, generate the token if he is valid user and I will then call my api.

Note: Currently I am using MVC Client but I will add one more client later on, may be Angular.

But I have a questions here.
How can I add the users to my database and authenticate the database user not the Test one.
Another thing which I am not understanding is where I should put the Login and Register functionality and how that code will look like.

I am new to identity server please excuse me.

Can anybody guide me on my above question with some code ? Thanks in advance

Upvotes: 0

Views: 6061

Answers (3)

g_b
g_b

Reputation: 21

How can I add the users to my database and authenticate the database user not the Test one.

Another thing which I am not understanding is where I should put the Login and Register functionality and how that code will look like.

There is a way to create a working example complying your requirements using mainly implementations from IdentityServer4 Quickstarts.

The steps are(using SQL Database):

  1. Create mvc core project using dotnet is4aspid template. It will configure IdentityServer as a middleware for the project, you can update database with ready migration to create all tables for ASP.NET Core Identity and Login, Logout, Consent, Grants (UI) functionality for IdentityServer. (In CreaeteIdentitySchema.cs file before db update replace Annotation for Identity column to comply SQL database as: Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn) , schema in the template is for SQLite database)

  2. Activate Razore Pages in Startup.cs of mvc core project, adding services.AddRazorPages() and endpoints.MapRazorPages(), then it will be possible to add scaffolding and you can add all pages you need to register and maintain user accounts (local and from external providers) using Razor Class Library. Login and Logout pages should stay under control of IdentityServer for authentication purpose.

  3. Next you can use ConfigurationDbContext , PersistedGrantDbContext contexts and their entities from IdentityServer4.EntityFramework.Storage nuget package to create migration and update existing ASP.NET Identity database, adding tables for clients, resources, and scopes, also for temporary operational data such as authorization codes and refresh tokens. To add, remove or update data to these tables you can manually create interface using these two contexts.

  4. Last step is to create client and Api projects according to Quickstarts and configure them with IdentityServer to use.

Startup.cs file at the end will be:

    public class Startup
{
    public IWebHostEnvironment Environment { get; }
    public IConfiguration Configuration { get; }

    public Startup(IWebHostEnvironment environment, IConfiguration configuration)
    {
        Environment = environment;
        Configuration = configuration;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IEmailSender, EmailSender>();

        services.AddControllersWithViews();

        services.Configure<IISOptions>(iis =>
        {
            iis.AuthenticationDisplayName = "Windows";
            iis.AutomaticAuthentication = false;
        });

        services.Configure<IISServerOptions>(iis =>
        {
            iis.AuthenticationDisplayName = "Windows";
            iis.AutomaticAuthentication = false;
        });

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

        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

        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;
            })
            .AddConfigurationStore(options =>
            {
                options.ConfigureDbContext = b =>   b.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                    sql => sql.MigrationsAssembly(migrationsAssembly));
            })
            .AddOperationalStore(options =>
            {
                options.ConfigureDbContext = b =>           b.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),
                    sql => sql.MigrationsAssembly(migrationsAssembly));
            })
            .AddAspNetIdentity<ApplicationUser>();

        builder.AddDeveloperSigningCredential();

        services.AddRazorPages();

        services.AddAuthentication()
            .AddGoogle(options =>
            {
                options.ClientId = "copy client ID from Google here";
                options.ClientSecret = "copy client secret from Google here";
            });
    }

    public void Configure(IApplicationBuilder app)
    {
        if (Environment.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }

        app.UseStaticFiles();

        app.UseRouting();
        app.UseIdentityServer();
        app.UseAuthorization();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapRazorPages();
        });
    }

}  

Upvotes: 0

Always_a_learner
Always_a_learner

Reputation: 1304

In ResourceOwnerPasswrod flow, you could keep register and login functionality at client side and you can validate user against database user.

You should implement custom user store for validating user and adding claims from database. Change startup code like below, Userrepository class represents database communication to authenticate user and get claims from database:

Update Start up Configuration method for identity configuration:

var idServerServiceFactory = new IdentityServerServiceFactory()
.UseInMemoryClients(Clients.Get())
.UseInMemoryScopes(Scopes.Get())
.AddCustomUserStore();

Add below classes and change according to your requirement:

public static class CustomIdentityServerBuilderExtensions
{
    public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
    {                   
        builder.AddProfileService<UserProfileService>();           
        builder.AddResourceOwnerValidator<UserResourceOwnerPasswordValidator>();
        return builder;
    }
}

public class UserProfileService : IProfileService
{
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
            UserRepository userRepository=new UserRepository();
            var user = userRepository.GetUserById(int.Parse(context.Subject.GetSubjectId()));
            if (user != null)
            {
                var userTokenModel = _mapper.Map<UserTokenModel>(user);
                var claims = new List<Claim>();
                claims.Add(new Claim("UserId", user.UserId));
                // Add another claims here 
                context.IssuedClaims.AddRange(claims);                    
    }
    public async Task IsActiveAsync(IsActiveContext context)
    {          
    }
}

public class UserResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{        
    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {           
            UserRepository userRepository=new UserRepository();
            var userLoginStatus = userRepository.GetUserById(context.UserName, context.Password);

            if (userLoginStatus != null)
            {

                    context.Result = new GrantValidationResult(userLoginStatus.UserId.ToString(),
                         OidcConstants.AuthenticationMethods.Password);                   
            }
            else
            {                    
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidClient, 
                        "Wrong Credentials");
            }            
    }
}

Refer ASP.NET CORE IDENTITYSERVER4 RESOURCE OWNER PASSWORD FLOW WITH CUSTOM USERREPOSITORY for ResourceOwnerPasswrod flow. This flow is recommend to use for supporting old applications.

There are more Flows :

  1. Implicit
  2. Hybrid
  3. Authorize code

Refer official documentation for more details.

Upvotes: 0

Kishan Vaishnav
Kishan Vaishnav

Reputation: 2631

The responsibility of creating and maintaining users is of the Authentication Server.

Where I should put the Login and Register functionality

So, the Identity Server project would contain endpoints like Register, Login, Forgot password, etc.

How can I add the users to my database and authenticate the database user not the Test one.

Microsoft Identity Core

You can implement Microsoft Identity Core which provides all the functionalities related to Account management. And there is built-in support for it in the IdentityServer4.

This way you would not have to worry about code or the database.

Note: The Microsoft Identity Core does a lot of things under the hood so you won't be able to understand how actually it works with IdentityServer4.

You can find the sample code from here (Open Startup.cs) and documentation from here.

You can also take a look at this YouTube series by Row Coding.

Custom user repository

If you want to validate users without using Microsoft Identity Core then you can implement IResourceOwnerPasswordValidator interface, sample code can be found here here and blog from here.

Upvotes: 1

Related Questions