一个项目中的IdentityServer4和Web Api无法通过身份验证 [英] IdentityServer4 and Web Api in one project fails to authenticate
问题描述
所以在我的解决方案中,我们有3个项目
API
- 生产API资源
IdentityServer4
- IdentityServer4
- 用于访问IdentityServ4上的客户端,范围等的WebAPI管理API
客户端APP
- MVC应用程序
一切都很好.客户端可以通过IS4登录并进行身份验证,并访问生产资源.现在,还需要创建api来从客户端应用程序管理IS4.但似乎我无法使用IS4发行的相同令牌进行身份验证.
IS4日志上的消息如下
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Route matched with {action = "GetUserAccountsList", controller = "Accounts"}. Executing action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API)
dbug: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[9]
AuthenticationScheme: Bearer was not authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
Executing ChallengeResult with authentication schemes (Bearer).
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: BearerIdentityServerAuthenticationJwt was challenged.
info: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) in 0.212ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.6503ms 401
IS4 Web API上的API代码
[Authorize(AuthenticationSchemes = "Bearer")]
[HttpGet]
public async Task<IActionResult> GetUserAccountsList()
{
var userAccounts = await _accountService.GetIdentityAccountsAsync();
return new JsonResult(userAccounts);
}
在启动时 ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
var dbConnectionName = Constants.Environment.Development;
if (_env.IsProduction())
{
dbConnectionName = Constants.Environment.Production;
}
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString(dbConnectionName), sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
}));
services.AddIdentity<ApplicationUser, IdentityRole>()
// use this if we want to implement default ASP.NET identity
//services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure DI
ConfigureDependencies(services);
services.AddMvc();
#region Registering ASP.NET Identity Server
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer(options =>
{
options.IssuerUri = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
options.Authentication.CookieLifetime = TimeSpan.FromHours(2);
})
// change to certificate credentials on production
// .AddSigningCredential(Certificate.Get())
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
//// this adds the config data from DB (clients, resources) instead of memory
//.AddInMemoryIdentityResources(Config.GetIdentityResources())
//.AddInMemoryClients(Config.GetClients(_env.IsProduction()))
//.AddInMemoryApiResources(Config.GetApiResources())
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
sql => sql.MigrationsAssembly(migrationsAssembly));
})
//// this adds the operational data from DB (codes, tokens, consents)
//.AddInMemoryPersistedGrants()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
})
.AddProfileService<IdentityProfileService>(); ;
#endregion Registering ASP.NET Identity Server
services.RegisterApplicationPolicy();
#region External Auth
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
options.RequireHttpsMetadata = false;
options.ApiName = Constants.Resource.Identity;
// options.SupportedTokens = SupportedTokens.Both;
}); ;
#endregion External Auth
}
Startup.cs 配置
public void Configure(IApplicationBuilder app, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
InitializeDatabase(app, _env, userManager, roleManager);
if (_env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseIdentityServer();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
将IS4上的客户端播种到数据库并按以下步骤进行设置
public static class Resource
{
public static List<string> GetAllResourceList()
{
return new List<string>()
{
Clinic,
Subscription,
Module,
Identity // this is IDS4 Server
};
}
// this is used on db seed only.
// resource name has to be updated on DB after db seed
public const string Clinic = "Clinic";
public const string Subscription = "Subscription";
public const string Module = "Module";
public const string Identity = "Identity";
public const string ClinicAddress = "http://localhost:5100";
public const string SubscriptionAddress = "http://localhost:5200";
public const string ModuleAddress = "http://localhost:5300";
public const string IdentityAddress = "http://localhost:5000";
}
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return Constants.Resource.GetAllResourceList().Select(s => new ApiResource(s));
}
// client want to access resources (aka scopes)
public static IEnumerable<Client> GetClients(bool isDevelopment)
{
var client = new List<Client>();
var mvcClient = new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { $"{Constants.Address.GetClientServerAdress(isDevelopment)}/signin-oidc" },
PostLogoutRedirectUris =
{$"{Constants.Address.GetClientServerAdress(isDevelopment)}/signout-callback-oidc"},
AlwaysIncludeUserClaimsInIdToken = true,
AllowAccessTokensViaBrowser = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
},
AllowOfflineAccess = true
};
foreach (var resource in Constants.Resource.GetAllResourceList())
{
mvcClient.AllowedScopes.Add(resource);
}
client.Add(mvcClient);
return client;
}
//Add support for the standard openid (subject id) and profile scopes
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
}
在客户端应用上 Startup.cs如下
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
// Adding Authentication options+
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = $"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
foreach (var resource in Constants.Resource.GetAllResourceList())
{
options.Scope.Add(resource);
}
options.Scope.Add("offline_access");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
};
});
// Adding Authorisation
services.RegisterApplicationPolicy();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseAuthentication();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
我们尝试使用以下代码访问IS4上的Web Api.看到下面的调用总是返回401 Unauthorize
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(accessToken);
var response = await client.GetAsync($"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}/api/Accounts/GetUserAccountsList");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
ViewBag.Json = JArray.Parse(content).ToString();
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
return Unauthorized();
}
return View("Json");
有关如何解决该问题的任何建议都将有所帮助.目前,我正在使用表单身份验证在IS4上进行客户端管理.
在使用提琴手检查已发送的请求之后,我终于找出了请求的原因.范围和资源设置正确.原因是IdentityServer身份验证正在比较令牌发行者.
客户的两个部门和身份指向IDS http:localhost:5000而不是https.因此令牌发行者设置为http.所以我只需要将权限更改为https.我的一个愚蠢的错误=).
由于某些原因,WebApi在IDS和Resource APIS上的authorize属性在IDS检查颁发者和资源不起作用的情况下表现不同.必须在这个问题上做更多的研究.
im stuck on finding the issue i have here. i have tried to find from questions in SO but could figure out the problem. so im quite desperate atm.
so in my solutions we have 3 projects
API
- Production API Resource
IdentityServer4
- IdentityServer4
- WebAPI Management API to access Client, Scopes, Etc on IdentityServ4
Client APP
- MVC App
Everything went fine. Client can login and authenticate via IS4 and access production resources. there is now comes a need to also create api to manage IS4 from the client app as well. but it seems that i cant Authenticate using the same token issued by the IS4.
the message on IS4 Log is as follows
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[1]
Route matched with {action = "GetUserAccountsList", controller = "Accounts"}. Executing action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API)
dbug: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[9]
AuthenticationScheme: Bearer was not authenticated.
info: Microsoft.AspNetCore.Authorization.DefaultAuthorizationService[2]
Authorization failed.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[3]
Authorization failed for the request at filter 'Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter'.
info: Microsoft.AspNetCore.Mvc.ChallengeResult[1]
Executing ChallengeResult with authentication schemes (Bearer).
info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[12]
AuthenticationScheme: BearerIdentityServerAuthenticationJwt was challenged.
info: IdentityServer4.AccessTokenValidation.IdentityServerAuthenticationHandler[12]
AuthenticationScheme: Bearer was challenged.
info: Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker[2]
Executed action Identity.API.API.AccountsController.GetUserAccountsList (Identity.API) in 0.212ms
info: Microsoft.AspNetCore.Hosting.Internal.WebHost[2]
Request finished in 0.6503ms 401
API Code on IS4 Web API
[Authorize(AuthenticationSchemes = "Bearer")]
[HttpGet]
public async Task<IActionResult> GetUserAccountsList()
{
var userAccounts = await _accountService.GetIdentityAccountsAsync();
return new JsonResult(userAccounts);
}
And On StartUp ConfigureServices
public void ConfigureServices(IServiceCollection services)
{
var dbConnectionName = Constants.Environment.Development;
if (_env.IsProduction())
{
dbConnectionName = Constants.Environment.Production;
}
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString(dbConnectionName), sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
}));
services.AddIdentity<ApplicationUser, IdentityRole>()
// use this if we want to implement default ASP.NET identity
//services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddRoleManager<RoleManager<IdentityRole>>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
// Configure DI
ConfigureDependencies(services);
services.AddMvc();
#region Registering ASP.NET Identity Server
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
services.AddIdentityServer(options =>
{
options.IssuerUri = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
options.Authentication.CookieLifetime = TimeSpan.FromHours(2);
})
// change to certificate credentials on production
// .AddSigningCredential(Certificate.Get())
.AddDeveloperSigningCredential()
.AddAspNetIdentity<ApplicationUser>()
//// this adds the config data from DB (clients, resources) instead of memory
//.AddInMemoryIdentityResources(Config.GetIdentityResources())
//.AddInMemoryClients(Config.GetClients(_env.IsProduction()))
//.AddInMemoryApiResources(Config.GetApiResources())
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
sql => sql.MigrationsAssembly(migrationsAssembly));
})
//// this adds the operational data from DB (codes, tokens, consents)
//.AddInMemoryPersistedGrants()
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder =>
builder.UseSqlServer(Configuration.GetConnectionString(dbConnectionName),
sql => sql.MigrationsAssembly(migrationsAssembly));
// this enables automatic token cleanup. this is optional.
options.EnableTokenCleanup = true;
options.TokenCleanupInterval = 30;
})
.AddProfileService<IdentityProfileService>(); ;
#endregion Registering ASP.NET Identity Server
services.RegisterApplicationPolicy();
#region External Auth
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Constants.Address.GetIdentityServerAdress(_env.IsDevelopment());
options.RequireHttpsMetadata = false;
options.ApiName = Constants.Resource.Identity;
// options.SupportedTokens = SupportedTokens.Both;
}); ;
#endregion External Auth
}
Startup.cs Configure
public void Configure(IApplicationBuilder app, UserManager<ApplicationUser> userManager, RoleManager<IdentityRole> roleManager)
{
InitializeDatabase(app, _env, userManager, roleManager);
if (_env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseIdentityServer();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Client on IS4 is seeded to DB and set up as follows
public static class Resource
{
public static List<string> GetAllResourceList()
{
return new List<string>()
{
Clinic,
Subscription,
Module,
Identity // this is IDS4 Server
};
}
// this is used on db seed only.
// resource name has to be updated on DB after db seed
public const string Clinic = "Clinic";
public const string Subscription = "Subscription";
public const string Module = "Module";
public const string Identity = "Identity";
public const string ClinicAddress = "http://localhost:5100";
public const string SubscriptionAddress = "http://localhost:5200";
public const string ModuleAddress = "http://localhost:5300";
public const string IdentityAddress = "http://localhost:5000";
}
public class Config
{
public static IEnumerable<ApiResource> GetApiResources()
{
return Constants.Resource.GetAllResourceList().Select(s => new ApiResource(s));
}
// client want to access resources (aka scopes)
public static IEnumerable<Client> GetClients(bool isDevelopment)
{
var client = new List<Client>();
var mvcClient = new Client
{
ClientId = "mvc",
ClientName = "MVC Client",
AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,
RequireConsent = false,
ClientSecrets =
{
new Secret("secret".Sha256())
},
RedirectUris = { $"{Constants.Address.GetClientServerAdress(isDevelopment)}/signin-oidc" },
PostLogoutRedirectUris =
{$"{Constants.Address.GetClientServerAdress(isDevelopment)}/signout-callback-oidc"},
AlwaysIncludeUserClaimsInIdToken = true,
AllowAccessTokensViaBrowser = true,
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
},
AllowOfflineAccess = true
};
foreach (var resource in Constants.Resource.GetAllResourceList())
{
mvcClient.AllowedScopes.Add(resource);
}
client.Add(mvcClient);
return client;
}
//Add support for the standard openid (subject id) and profile scopes
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new List<IdentityResource>
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
}
}
On Client App Startup.cs is as follows
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
// Adding Authentication options+
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.SignInScheme = "Cookies";
options.Authority = $"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code id_token";
options.SaveTokens = true;
options.GetClaimsFromUserInfoEndpoint = true;
options.RequireHttpsMetadata = false;
foreach (var resource in Constants.Resource.GetAllResourceList())
{
options.Scope.Add(resource);
}
options.Scope.Add("offline_access");
options.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = "name",
RoleClaimType = "role",
};
});
// Adding Authorisation
services.RegisterApplicationPolicy();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseAuthentication();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
where we are trying to access Web Api on IS4 with the following code. See that the following call always return 401 Unauthorize
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.SetBearerToken(accessToken);
var response = await client.GetAsync($"{Constants.Address.GetIdentityServerAdress(_env.IsDevelopment())}/api/Accounts/GetUserAccountsList");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
ViewBag.Json = JArray.Parse(content).ToString();
}
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
return Unauthorized();
}
return View("Json");
Any advice on how to fix the problem will be helpfull. at the moment im using form authentication to do client management on IS4.
I finally figure out the cause of the request after checking the sent request using fiddler. the scope and resources setting was correct. The cause is identityServer authentication is comparing the token issuer.
both authority on client & identity is pointing to IDS http:localhost:5000 instead of https. therefore the token issuer is set as http. so i only need to change the authority to https. what a silly mistake of mine =).
for some reason WebApi's authorize attribute on IDS and Resource APIS behave differently where IDS checking issuer and resources doest. have to do more research on this issue.
这篇关于一个项目中的IdentityServer4和Web Api无法通过身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!