如何在 Identity Server 4 中验证 AAD 用户?

How to authenticate AAD user in Identity Server 4?

我已将 Azure Active Directory 作为外部提供程序集成到 Identity Server 4 中。

我想使用 API 从 Identity Server 4 对 Azure Active Directory 用户进行身份验证。

代码如下:

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

当我执行这段代码时,出现了无效的用户名或密码错误。

注意:提供的凭据有效。

这是我的 startup.cs 文件

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

默认情况下 Resource Owner Password 工作流使用 connect/token 端点验证 AspNetUser table 中存在的用户(它不会验证外部用户),这就是您收到 Invalid User name or password 错误消息的原因,因为您的用户存在于 Microsoft AD 而不是 ID4 数据库中。

不推荐使用 Resource Owner Password 密码登录,但如果您仍然需要它,您可以覆盖端点实现并编写您自己的自定义逻辑以使用 Microsoft AD 数据对用户进行身份验证来源。

我搜索并找到了一些可能的方法来自定义来自外部提供商的用户名和密码。以下是步骤

1- 实现如下所示的 class

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- 然后在 startup.cs 文件中注册此接口,如下所示:

services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();

因此,当您尝试调用端点 /connect/token 时,将触发此 class。