基于IdentityServer4的多服务授权配置问题求助
Alright, let's break this down—you're running into a common pitfall with IdentityServer4 flow selection. Here's what's wrong and how to fix it:
The core issue is that you've configured both Service A and B as Implicit flow clients, but Implicit flow is designed for browser-based/frontend apps, not server-to-server API calls. For Service A to call Service B's authorized endpoints without forcing the user to re-authenticate, we need to reconfigure B as an API Resource in IdentityServer, and set up A to request and pass a valid access token when calling B.
Step 1: Update IdentityServer4 Configuration
First, we'll register Service B as an API resource and update Service A's client settings to allow it to access B's API.
Configure Service B as an API Resource
In your IdentityServer's Config.cs (or equivalent config class), add Service B as an API resource:
public static IEnumerable<ApiResource> GetApiResources() { return new List<ApiResource> { new ApiResource("service_b_api", "Service B Protected API") { // Add granular scopes if you need fine-grained access control Scopes = { new Scope("service_b_api.full_access") } } }; }
Update Service A's Client Configuration
Modify Service A's client setup to use Authorization Code flow (more secure for server-side apps than Implicit) and include the scope for Service B's API:
public static IEnumerable<Client> GetClients() { return new List<Client> { new Client { ClientId = "service_a", ClientName = "Service A (Server-Side App)", AllowedGrantTypes = GrantTypes.Code, ClientSecrets = { new Secret("service_a_secure_secret".Sha256()) }, RedirectUris = { "https://localhost:xxxx/signin-oidc" }, // Your A's OIDC callback URL PostLogoutRedirectUris = { "https://localhost:xxxx/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "service_b_api.full_access" // Grant access to Service B's API }, AllowOfflineAccess = true, // Enable refresh tokens for longer-lived access AccessTokenLifetime = 3600 // Adjust token expiry as needed } }; }
Step 2: Configure Service A to Pass Access Tokens to Service B
Service A needs to retrieve a valid access token for Service B's API and attach it to outgoing requests. We'll use an HTTP client handler to automate this.
Update Service A's Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddHttpContextAccessor(); // Required to access current user's tokens services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie() .AddOpenIdConnect(options => { options.Authority = "https://localhost:yyyy"; // Your IdentityServer URL options.ClientId = "service_a"; options.ClientSecret = "service_a_secure_secret"; options.ResponseType = "code"; // Use Authorization Code flow options.Scope.Add("service_b_api.full_access"); // Request access to B's API options.SaveTokens = true; // Store tokens in the authentication cookie options.GetClaimsFromUserInfoEndpoint = true; }); // Register a named HTTP client for Service B, with token attachment logic services.AddHttpClient("ServiceBClient", client => { client.BaseAddress = new Uri("https://localhost:zzzz/"); // Service B's base URL }) .AddHttpMessageHandler<BearerTokenAttachmentHandler>(); // Register the custom handler to attach access tokens services.AddTransient<BearerTokenAttachmentHandler>(); } // Custom handler to automatically add the Bearer token to requests public class BearerTokenAttachmentHandler : DelegatingHandler { private readonly IHttpContextAccessor _httpContextAccessor; public BearerTokenAttachmentHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync("access_token"); if (!string.IsNullOrEmpty(accessToken)) { request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } return await base.SendAsync(request, cancellationToken); } }
Use the Client in Service A's Controller
public class HomeController : Controller { private readonly IHttpClientFactory _httpClientFactory; public HomeController(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } public async Task<IActionResult> CallProtectedServiceB() { var client = _httpClientFactory.CreateClient("ServiceBClient"); var response = await client.GetAsync("api/protected-data"); response.EnsureSuccessStatusCode(); var content = await response.Content.ReadAsStringAsync(); return View("ServiceBResult", content); } }
Step 3: Configure Service B to Validate IdentityServer Tokens
Service B needs to accept and validate the Bearer token from Service A using JWT Bearer authentication.
Update Service B's Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddControllersWithViews(); services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Authority = "https://localhost:yyyy"; // Your IdentityServer URL options.Audience = "service_b_api"; // Must match the API resource name in IdentityServer options.RequireHttpsMetadata = true; // Disable only in development }); // Set up default authorization policy services.AddAuthorization(options => { options.DefaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .Build(); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ... other middleware (exception handling, static files, etc.) app.UseRouting(); app.UseAuthentication(); // Must come before UseAuthorization app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); }); }
Step 4: Use [Authorize] in Service B
Now you can safely add the [Authorize] attribute to Service B's controllers/actions—it will validate the Bearer token from Service A:
[ApiController] [Route("api")] public class ProtectedDataController : ControllerBase { [Authorize] [HttpGet("protected-data")] public IActionResult GetProtectedData() { var userName = User.Identity.Name; var userRoles = User.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value); return Ok($"Hello {userName}! Your roles: {string.Join(", ", userRoles)} (from Service B's protected endpoint)"); } }
Key Takeaways
- Why Implicit Flow failed: Implicit flow returns tokens directly to browsers, which aren't designed for server-side services to store and use for API calls. Authorization Code flow is the secure choice for server-to-server scenarios.
[Authorize]works perfectly here: As long as Service B can validate the incoming Bearer token, the[Authorize]attribute will restrict access to authenticated users exactly as intended.- Granular access control: If you need to limit Service A's access to specific parts of Service B, define more scopes in IdentityServer and use
[Authorize(Policy = "RequireAdminScope")]with a policy that checks the scope claim.
内容的提问来源于stack exchange,提问作者Marcin

