简体   繁体   English

IdentityServer4 使用带有 API 的客户端凭据工作流(或尝试模拟 OIDC 调用)

[英]IdentityServer4 Using Client Credential Workflow with an API (Or trying to emulate OIDC calls)

I am wanting to use IdentityServer4 to secure APIs using Windows Credentials.我想使用 IdentityServer4 来保护使用 Windows 凭据的 API。 I have created a working example in a web application, but trying to mimic the OIDC calls is proving troublesome.我在 web 应用程序中创建了一个工作示例,但尝试模仿 OIDC 调用证明很麻烦。 In the documents it appears to suggest that the only way to work with an API is to authenticate with a ClientID and secret.文件中,似乎表明使用 API 的唯一方法是使用 ClientID 和密钥进行身份验证。 I wanted to see if this was true.我想看看这是不是真的。 Below I will add my network calls I am currently doing to try and emulate the OIDC workflow.下面我将添加我目前正在尝试模拟 OIDC 工作流程的网络调用。 Hopefully there is either a better way to approach this problem or a simpler set of calls.希望有更好的方法来解决这个问题或一组更简单的调用。 I appreciate help either way.无论哪种方式,我都很感激帮助。

Minimal working example (All calls made through Postman)最小的工作示例(所有通过邮递员拨打的电话)

  1. I call the login endpoint "[GET] https://localhost:44353/Account/Login", this returns a 200 OK login page HTML and more importantly my ".AspNetCore.Antiforgery" cookie我调用登录端点“[GET] https://localhost:44353/Account/Login”,这将返回一个 200 OK 登录页面 HTML,更重要的是我的“.AspNetCore.Antiforgery”cookie

  2. I call my challenge endpoint "[GET] https://localhost:44353/External/Challenge?provider=Windows" using NTLM Authentication and providing my windows credentials.我使用 NTLM 身份验证调用我的挑战端点“[GET] https://localhost:44353/External/Challenge?provider=Windows”并提供我的 windows 凭据。 This returns a 401 Unauthorized and a cookie "idsrv.external", I think the 401 is just due to a redirect, I actually just need the cookie.这将返回 401 Unauthorized 和 cookie “idsrv.external”,我认为 401 只是由于重定向,我实际上只需要 cookie。

  3. I call the callback endpoint "[GET] https://localhost:44353/External/Callback" and that deletes my "idsrv.external" cookie and sets cookies called "idsrv.session" and "idsrv".我调用回调端点“[GET] https://localhost:44353/External/Callback”并删除我的“idsrv.external”cookie 并将 cookies 称为“idsrv.session”和“idsrv”。

  4. I now try and call my API endpoint "[GET] https://localhost:16385/managementservice/schema" using the cookies I have received so far.我现在尝试使用我目前收到的 cookies 调用我的 API 端点“[GET] https://localhost:16385/managementservice/schema”。 This returns to me the OIDC permissions request page.这将返回给我 OIDC 权限请求页面。

  5. I take the return URL and token from the html of the last request and I call "[POST] https://localhost:44353/Consent" with the form data below.我从最后一个请求的 html 中获取返回 URL 和令牌,并使用以下表格调用“[POST] https://localhost:44353/Consent” This returns 200 OK html with a button that calls "https://localhost:16385/signin-oidc".这将返回 200 OK html 并带有一个调用“https://localhost:16385/signin-oidc”的按钮。

图片

  1. I use the data from the last html to then call "[POST] https://localhost:16385/signin-oidc" as shown below.我使用上一个 html 中的数据然后调用“[POST] https://localhost:16385/signin-oidc”,如下所示。 I have 5 cookies set;我有 5 个 cookies 组; .AspNetCore.OpenIdConnect.Nonce, .AspNetCore.Correlation.oidc, .AspNetCore.Antiforgery, idsrv.session, and idsrv. .AspNetCore.OpenIdConnect.Nonce、.AspNetCore.Correlation.oidc、.AspNetCore.Antiforgery、idsrv.session 和 idsrv。 However, this call returns a 500 Internal Server Error instead of a 302 Found like the UI.但是,此调用返回 500 Internal Server Error 而不是像 UI 一样的 302 Found。 Any ideas on what is going wrong here?关于这里出了什么问题的任何想法?

图片

I can provide more data or specific files as needed.我可以根据需要提供更多数据或特定文件。 This is just a jumping off point.这只是一个起点。

EDIT: I received a request to provide applicable files.编辑:我收到了提供适用文件的请求。 My client application is a ASP.NET Core API that I am htting with postman.我的客户端应用程序是 ASP.NET 核心 API,我正在使用 postman。

IdentityServer Startup.cs身份服务器启动.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身份服务器配置.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 Service Startup.cs 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();
            });
        }
    }
}

Don'tknow if its the issue but, one issue is that you have in IdentityServer不知道是否是问题,但是,一个问题是您在 IdentityServer 中

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

See this article about how to configure the pipeline.请参阅这篇文章,了解如何配置管道。

Especially, take notice about that it says:特别是,请注意它说:

UseIdentityServer includes a call to UseAuthentication, so it's not necessary to have both. UseIdentityServer 包括对 UseAuthentication 的调用,因此不必同时拥有这两者。

As I said in the comments, trying to send a request to /signin-oidc from postman will probably fail due to various built in features in how authentication works.正如我在评论中所说,尝试从 postman 向 /signin-oidc 发送请求可能会由于身份验证工作方式中的各种内置功能而失败。 One problem is that you don't have the correct state parameter that the OpenIdConnect handler expects.一个问题是您没有 OpenIdConnect 处理程序期望的正确 state 参数。 Its a random value that changes each time a user tries to authenticate.它是一个随机值,每次用户尝试进行身份验证时都会更改。

Your "ASP.NET API Service Startup.cs", is a "client", not an API.您的“ASP.NET API Service Startup.cs”是“客户端”,而不是 API。 What you have is meant for a end-user to login to.您拥有的内容是供最终用户登录的。 Usin postman here makes no sense.在这里使用 postman 没有任何意义。 An API should probably use the UseJwtBearer handler instead and to that one you could send requests using PostMan and a valid access-token. API 可能应该使用 UseJwtBearer 处理程序,并且您可以使用 PostMan 和有效的访问令牌向该处理程序发送请求。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM