How to authenticate AAD user in Identity Server 4?

Viewed 908

I have integrated the Azure Active Directory in Identity Server 4 as an external provider.

I want to authenticate Azure Active Directory users from Identity Server 4 by using the APIs.

Here is the code:

var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");

if (disco.IsError)
{
    Console.WriteLine(disco.Error);
    return;
}

// request token
var tokenResponse = await client.RequestPasswordTokenAsync(new PasswordTokenRequest
        {
            Address = disco.TokenEndpoint,
            ClientId = "client",
            ClientSecret = "secret",

            Scope = "api1",
            UserName = "ad user name",
            Password = "add user password"
        });
        

When I execute this piece of code, I got an invalid username or password error.

Note: the provided credentials are valid.

Here is my startup.cs file

using IdentityServer4;
using IdentityServer4.EntityFramework.DbContexts;
using IdentityServer4.EntityFramework.Mappers;
using MorpheusIdentityServer.Quickstart.UI;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using IdentityServer4.Validation;

namespace MorpheusIdentityServer
{
    public class Startup
    {
        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllersWithViews();

            var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
            string connectionString = Configuration.GetSection("ConnectionString").Value;

            var builder = services.AddIdentityServer()
                .AddConfigurationStore(options =>
                {
                    options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                .AddOperationalStore(options =>
                {
                    options.ConfigureDbContext = b => b.UseSqlServer(connectionString,
                        sql => sql.MigrationsAssembly(migrationsAssembly));
                })
                .AddProfileService<ProfileService>();

                 

            services.AddAuthentication()
                 .AddOpenIdConnect("aad", "Azure AD", options =>
                 {

                     options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
                     options.SignOutScheme = IdentityServerConstants.SignoutScheme;

                     options.Authority = Configuration.GetSection("ActiveDirectoryAuthority").Value;
                     options.ClientId = Configuration.GetSection("ActiveDirectoryClientId").Value;
                     options.ResponseType = OpenIdConnectResponseType.IdToken;
                     options.CallbackPath = "/signin-aad";
                     options.SignedOutCallbackPath = "/signout-callback-aad";
                     options.RemoteSignOutPath = "/signout-aad";
                     options.TokenValidationParameters = new TokenValidationParameters
                     {
                         NameClaimType = "name",
                         RoleClaimType = "role",
                         ValidateIssuer = false
                     };

                 });
            services.AddSingleton<IResourceOwnerPasswordValidator, ValidateExternalUser>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            InitializeDatabase(app);
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseRouting();

            app.UseIdentityServer();
            app.UseAuthorization();

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

        private void InitializeDatabase(IApplicationBuilder app)
        {
            using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
            {
                serviceScope.ServiceProvider.GetRequiredService<PersistedGrantDbContext>().Database.Migrate();

                var context = serviceScope.ServiceProvider.GetRequiredService<ConfigurationDbContext>();
                context.Database.Migrate();
                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 resource in Config.ApiScopes)
                    {
                        context.ApiScopes.Add(resource.ToEntity());
                    }
                    context.SaveChanges();
                }
            }
        }
    }
}
2 Answers

By default Resource Owner Password workflow uses the connect/token endpoint which validates the users who are present in the AspNetUser table only (it will not validate external users), so that's the reason you are getting the Invalid User name or password error message because your user is present in Microsoft AD and not in ID4 database.

Log in using the Resource Owner Password password isn't the recommended approach but if you still need it anyway, you can override the endpoint implementation and write your own custom logic to authenticate the user with a Microsoft AD data source.

I have searched and found some possible ways to customize usernames and passwords from external providers. Here are the steps

1- Implement a class like below

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
       var username= context.UserName;
       var password= context.Password;
      // write the code which will validate the username and password from external provider.    
    }
}

2- Then register this interface in a startup.cs file like below:

services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();

So, when you try to call the endpoint /connect/token this class will be triggered.

Related