【发布时间】:2020-11-20 16:25:38
【问题描述】:
我想使用 IdentityServer4 来保护使用 Windows 凭据的 API。我在 Web 应用程序中创建了一个工作示例,但试图模仿 OIDC 调用被证明很麻烦。在documents 中似乎暗示使用 API 的唯一方法是使用 ClientID 和密钥进行身份验证。我想看看这是不是真的。下面我将添加我目前正在尝试模拟 OIDC 工作流程的网络调用。希望有更好的方法来解决这个问题或一组更简单的调用。无论哪种方式,我都感谢您的帮助。
最小的工作示例(通过 Postman 进行的所有调用)
-
我调用登录端点“[GET] https://localhost:44353/Account/Login”,这将返回一个 200 OK 登录页面 HTML,更重要的是我的“.AspNetCore.Antiforgery”cookie
-
我使用 NTLM 身份验证并提供我的 Windows 凭据,将我的挑战端点称为“[GET] https://localhost:44353/External/Challenge?provider=Windows”。这会返回一个 401 Unauthorized 和一个 cookie “idsrv.external”,我认为 401 只是由于重定向,我实际上只需要 cookie。
-
我将回调端点称为“[GET] https://localhost:44353/External/Callback”,这会删除我的“idsrv.external”cookie 并设置名为“idsrv.session”和“idsrv”的cookie。
-
我现在尝试使用我目前收到的 cookie 调用我的 API 端点“[GET] https://localhost:16385/managementservice/schema”。这将返回给我 OIDC 权限请求页面。
-
我从最后一个请求的 html 中获取返回 URL 和令牌,并使用下面的表单数据调用“[POST] https://localhost:44353/Consent”。这将返回 200 OK html,并带有一个调用“https://localhost:16385/signin-oidc”的按钮。
- 我使用最后一个 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