0

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

3 Answers 3

1

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.

Sign up to request clarification or add additional context in comments.

Comments

0

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.

7 Comments

Thank you so much for this. I will create one repository and test this code. One more thing I would like to ask since I am making ecommerce app , which grant type I should use ? Am I using correct grant type here ? Thanks again !!
Are you creating new app or extending to older one .you can see recommendation for app type in official doc.
It's a new app. Ok I will check the doc.
@Glennsingh The biggest disadvantage of Resource Owner Flow is that the User and Authentication Server has to completely trust the Client App. If you are building the E-Commerce app then you are better off wit the Authorization Code flow
@Glennsingh Another reason the Resource Owner Flow is not recommended is that the Client App has to handle user passwords. Which is difficult from security perspective.
|
0

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

}  

3 Comments

I am really sorry but I did not understand the 2nd point at all. I remember last time I created a Razor MVC app using identity scaffold and that worked fine for me. But here I am confused. As per your guidance, I have created a public repo on GitHub and followed all the points you mentioned above. It's giving me error as "unauthorized client", I tested my code and I am able to generate the token and able to access the API calls but MVC client not working, really don't know why its happening.
In point 4, you have mentioned to create api and client project, I added that but did not understand the 2nd point. In Razor pages (mvc client) and created the "Login.cshtml" but nothing seems to be working for me. Can you please add some code in my repo or suggest me. Please help me, not understanding where I am going wrong. Repo: github.com/Ashwani44/Sol_Ecommerce_Demo
Glenn Singh you do not need Razor pages in mvc client. Client redirected (options.DefaultChallengeScheme = "oidc") unauthorized calls to AuthServer project where you have Login and Logout pages in Views/Account folder, they come with is4aspid template. In 2nd point I mentioned to scaffold Razor Pages in AuthServer project to add Register, Delete ... Pages in addition to existing Login and Logout pages which should stay under IdentityServer control. Areas\Identity\Pages\Account folder missing in your Demo.AuthSerever project.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.