【问题标题】:IdentityServer4 Using Client Credential Workflow with an API (Or trying to emulate OIDC calls)IdentityServer4 使用带有 API 的客户端凭据工作流(或尝试模拟 OIDC 调用)
【发布时间】:2020-11-20 16:25:38
【问题描述】:

我想使用 IdentityServer4 来保护使用 Windows 凭据的 API。我在 Web 应用程序中创建了一个工作示例,但试图模仿 OIDC 调用被证明很麻烦。在documents 中似乎暗示使用 API 的唯一方法是使用 ClientID 和密钥进行身份验证。我想看看这是不是真的。下面我将添加我目前正在尝试模拟 OIDC 工作流程的网络调用。希望有更好的方法来解决这个问题或一组更简单的调用。无论哪种方式,我都感谢您的帮助。

最小的工作示例(通过 Postman 进行的所有调用)

  1. 我调用登录端点“[GET] https://localhost:44353/Account/Login”,这将返回一个 200 OK 登录页面 HTML,更重要的是我的“.AspNetCore.Antiforgery”cookie

  2. 我使用 NTLM 身份验证并提供我的 Windows 凭据,将我的挑战端点称为“[GET] https://localhost:44353/External/Challenge?provider=Windows”。这会返回一个 401 Unauthorized 和一个 cookie “idsrv.external”,我认为 401 只是由于重定向,我实际上只需要 cookie。

  3. 我将回调端点称为“[GET] https://localhost:44353/External/Callback”,这会删除我的“idsrv.external”cookie 并设置名为“idsrv.session”和“idsrv”的cookie。

  4. 我现在尝试使用我目前收到的 cookie 调用我的 API 端点“[GET] https://localhost:16385/managementservice/schema”。这将返回给我 OIDC 权限请求页面。

  5. 我从最后一个请求的 html 中获取返回 URL 和令牌,并使用下面的表单数据调用“[POST] https://localhost:44353/Consent”。这将返回 200 OK html,并带有一个调用“https://localhost:16385/signin-oidc”的按钮。

  1. 我使用最后一个 html 中的数据然后调用“[POST] https://localhost:16385/signin-oidc”,如下所示。我有 5 个饼干组; .AspNetCore.OpenIdConnect.Nonce、.AspNetCore.Correlation.oidc、.AspNetCore.Antiforgery、idsrv.session 和 idsrv。但是,此调用返回 500 Internal Server Error 而不是像 UI 一样的 302 Found。关于这里出了什么问题的任何想法?

我可以根据需要提供更多数据或特定文件。这只是一个起点。

编辑:我收到了提供适用文件的请求。我的客户端应用程序是一个 ASP.NET Core API,我正在使用 postman。

IdentityServer Startup.cs

using IdentityModel;
using IdentityServer4;
using IdentityServer4.Quickstart.UI;
using IdentityServer4.Services;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;

namespace IdentityServerTemplate
{
    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.AddControllersWithViews();

            services.AddHttpClient();

            // configures IIS out-of-proc settings (see https://github.com/aspnet/AspNetCore/issues/14882)
            services.Configure<IISOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = true;
            });

            // configures IIS in-proc settings
            services.Configure<IISServerOptions>(iis =>
            {
                iis.AuthenticationDisplayName = "Windows";
                iis.AutomaticAuthentication = true;
            });

            var builder = services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseInformationEvents = true;
                options.Events.RaiseFailureEvents = true;
                options.Events.RaiseSuccessEvents = true;
            });
            //.AddTestUsers(TestUsers.Users);

            // in-memory, code config
            builder.AddInMemoryIdentityResources(Config.Ids);
            builder.AddInMemoryApiResources(Config.Apis);
            builder.AddInMemoryClients(Config.Clients);

            services.AddScoped<IProfileService, ADProfileService>();

            // or in-memory, json config
            //builder.AddInMemoryIdentityResources(Configuration.GetSection("IdentityResources"));
            //builder.AddInMemoryApiResources(Configuration.GetSection("ApiResources"));
            //builder.AddInMemoryClients(Configuration.GetSection("clients"));

            // not recommended for production - you need to store your key material somewhere secure
            builder.AddDeveloperSigningCredential();

            services.AddAuthentication();
                //.AddGoogle(options =>
                //{
                //    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

                //    // register your IdentityServer with Google at https://console.developers.google.com
                //    // enable the Google+ API
                //    // set the redirect URI to http://localhost:5000/signin-google
                //    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.UseStaticFiles();

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

IdentityServer Config.cs

using IdentityModel;
using IdentityServer4.Models;
using System.Collections.Generic;

namespace IdentityServerTemplate
{
    public static class Config
    {
        public static IEnumerable<IdentityResource> Ids =>
            new IdentityResource[]
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
                new IdentityResources.Email(),
                new IdentityResources.Address(),
            };


        public static IEnumerable<ApiResource> Apis =>
            new ApiResource[]
            {
                // new ApiResource("api1", "My API #1")

                new ApiResource("api1", "My API", new[] { JwtClaimTypes.Subject, JwtClaimTypes.Email, JwtClaimTypes.Address, "upn_custom"})
            };


        public static IEnumerable<Client> Clients =>
            new Client[]
            {
                // client credentials flow client
                new Client
                {
                    ClientId = "identity.server",
                    ClientName = "Identity Server Client",

                    AllowedGrantTypes = GrantTypes.ClientCredentials,
                    AlwaysIncludeUserClaimsInIdToken = true,
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    AllowedScopes = { "openid", "profile", "email", "address", "api1", "upn_custom" }
                },

                // MVC client using code flow + pkce
                new Client
                {
                    //ClientId = "mvc",
                    ClientId = "mvc.code",
                    ClientName = "MVC Client",

                    // Note
                    AlwaysIncludeUserClaimsInIdToken = true,

                    AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
                    //RequirePkce = true,
                    RequirePkce = false,
                    //ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    //RedirectUris = { "https://localhost:5003/signin-oidc" },
                    RedirectUris = { "https://localhost:5003/signin-oidc" },
                    FrontChannelLogoutUri = "https://localhost:5003/signout-oidc",
                    PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },

                    AllowOfflineAccess = true,
                    AllowedScopes = { "openid", "profile", "email", "address", "api1", "upn_custom" }
                },

                // MCW Appserver
                new Client
                {
                    //ClientId = "mvc",
                    ClientId = "mcw.appserver",
                    ClientName = "MCW AppServer",

                    // Note
                    AlwaysIncludeUserClaimsInIdToken = true,

                    AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
                    RequirePkce = false,
                    //RequirePkce = false,
                    //ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    //RedirectUris = { "http://localhost:16835/signin-oidc" },
                    RedirectUris = { "https://localhost:16385/signin-oidc" },
                    FrontChannelLogoutUri = "https://localhost:16835/signout-oidc",
                    PostLogoutRedirectUris = { "https://localhost:16835/signout-callback-oidc" },

                    AllowOfflineAccess = true,
                    AllowedScopes = { "openid", "profile", "email", "address", "api1", "upn_custom" }
                },

                // MVC client using code flow + pkce
                new Client
                {
                    //ClientId = "mvc",
                    ClientId = "ptp.appserv",
                    ClientName = "PTP AppServ",

                    // Note
                    AlwaysIncludeUserClaimsInIdToken = true,

                    AllowedGrantTypes = GrantTypes.CodeAndClientCredentials,
                    //RequirePkce = true,
                    RequirePkce = false,
                    //ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) },
                    ClientSecrets = { new Secret("secret".Sha256()) },

                    //RedirectUris = { "https://localhost:30001/signin-oidc" },
                    RedirectUris = { "https://localhost:30001/signin-oidc" },
                    FrontChannelLogoutUri = "https://localhost:30001/signout-oidc",
                    PostLogoutRedirectUris = { "https://localhost:30001/signout-callback-oidc" },

                    AllowOfflineAccess = true,
                    AllowedScopes = { "openid", "profile", "email", "address", "api1", "upn_custom" }
                },             

                // SPA client using code flow + pkce
                new Client
                {
                    ClientId = "spa",
                    ClientName = "SPA Client",
                    ClientUri = "http://identityserver.io",

                    AllowedGrantTypes = GrantTypes.Code,
                    RequirePkce = true,
                    RequireClientSecret = false,

                    RedirectUris =
                    {
                        "http://localhost:5002/index.html",
                        "http://localhost:5002/callback.html",
                        "http://localhost:5002/silent.html",
                        "http://localhost:5002/popup.html",
                    },

                    PostLogoutRedirectUris = { "http://localhost:5002/index.html" },
                    AllowedCorsOrigins = { "http://localhost:5002" },

                    AllowedScopes = { "openid", "profile", "api1" }
                }
            };
    }
}

ASP.NET API 服务启动.cs

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Tps.ManagedClaimsWell.ApplicationServer.AppServInternals;
using Tps.ManagedClaimsWell.ApplicationServer.DataAccess;
using Tps.ManagedClaimsWell.ApplicationServer.Utility;

namespace ManagedClaimsWell.ApplicationServer.Core
{
    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)
        {
            JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

            services.AddControllers()
                .AddNewtonsoftJson();

            services.AddHttpClient();

            var appServSettings = new AppServSettings(Configuration);

            ClaimsWellCache.Inst.Load(ClaimsWellSchemaData.Load, IdentityData.UpdateNameLastAccessed);

            services.AddSingleton<IDiscoveryCache>(r =>
            {
                var factory = r.GetRequiredService<IHttpClientFactory>();
                return new DiscoveryCache(Constants.Authority, () => factory.CreateClient());
            });

            //services.AddAuthentication(options =>
            //{
            //    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            //    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            //    options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
            //})
            //.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
            //{
            //    options.
            //    options.ExpireTimeSpan = TimeSpan.FromDays(1);
            //});

            services.AddAuthorization(options =>
            {
                options.AddPolicy("scope", policy =>
                {
                    policy.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme)
                        .RequireAuthenticatedUser()
                        .RequireClaim("scope", "api1");
                });
            });

            services.AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie(options =>
            {
                options.Cookie.Name = "idsrv";
            })
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = Constants.Authority;
                options.RequireHttpsMetadata = false;

                options.ClientId = "mcw.appserver";
                options.ClientSecret = "secret";

                // code flow + PKCE (PKCE is turned on by default)
                options.ResponseType = "code";
                options.UsePkce = true;

                options.Scope.Clear();
                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("email");
                options.Scope.Add("api1");
                ////options.Scope.Add("transaction:123");
                ////options.Scope.Add("transaction");
                options.Scope.Add("offline_access");

                // not mapped by default
                options.ClaimActions.MapJsonKey(JwtClaimTypes.WebSite, "website");

                // keeps id_token smaller
                options.GetClaimsFromUserInfoEndpoint = true;
                options.SaveTokens = true;

                var handler = new JwtSecurityTokenHandler();
                handler.InboundClaimTypeMap.Clear();
                options.SecurityTokenValidator = handler;

                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = JwtClaimTypes.Name,
                    RoleClaimType = JwtClaimTypes.Role,
                };
            });
        }

        // 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.UseHttpsRedirection();

            app.UseRouting();

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

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

【问题讨论】:

  • 我认为您还需要显示一些代码 (Startup.cs) 以及有关您的设置的更多信息。当您将请求发送到 /signin-iodc 时,您会向 openidconnect 处理程序发送一个请求,我认为该处理程序更喜欢自己完成所有操作,否则它可能会抱怨 state 参数等等。
  • @ToreNestenius 我添加了我的 IdentityServer Startup.cs 和 Config.cs 以及我的 ASP.NET API Startup.cs。如果您认为任何其他文件会有所帮助,请告诉我。

标签: c# asp.net-core identityserver4 openid-connect


【解决方案1】:

不知道是不是问题,但一个问题是您在 IdentityServer 中遇到了问题

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

请参阅此article,了解如何配置管道。

特别注意它说:

UseIdentityServer 包含对 UseAuthentication 的调用,因此它不是 两者都需要。

正如我在 cmets 中所说,尝试从邮递员向 /signin-oidc 发送请求可能会由于身份验证工作方式中的各种内置功能而失败。一个问题是您没有 OpenIdConnect 处理程序期望的正确状态参数。它是一个随机值,每次用户尝试进行身份验证时都会更改。

您的“ASP.NET API 服务 Startup.cs”是“客户端”,而不是 API。您拥有的内容是供最终用户登录的。在这里使用邮递员毫无意义。 API 可能应该使用 UseJwtBearer 处理程序,而您可以使用 PostMan 和有效的访问令牌向该处理程序发送请求。

【讨论】:

  • 我的用例是能够通过 HTTP 从服务调用我的 API,该服务将使用该服务用户的 Windows 凭据进行身份验证。我能否使用 Windows 凭据通过 UseJwtBearer 获取初始 JWT Bearer 令牌?我能够使用我的客户端 ID 和密码获得 JWT,但无法弄清楚 Windows 身份如何适应它。
  • UseJwtBearer 仅适用于从某人接收访问令牌的 API。它不会处理初始身份验证。我不是 Windows 凭据专家,但这部分只是 IdentityServer 中的一个问题,它使用它来验证用户,因此 IdentityServer 可以颁发令牌。
  • 是的,在初始身份验证后仅使用 Jwt 就很理想了。搞清楚这一点有点痛苦。
猜你喜欢
  • 1970-01-01
  • 2021-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-07
  • 2020-06-13
  • 2023-03-12
  • 1970-01-01
相关资源
最近更新 更多