基于Identity Server 4与Web API的WPF应用及OidcClient配置咨询
Hey there! Let's walk through your IdentityServer4 + WPF + Web API setup, covering key configuration checks, API call best practices, and common pitfalls to avoid.
First, let's validate and refine your existing OidcClient setup to ensure it's robust:
Client Registration Match
Double-check that your IdentityServer4 instance athttp://localhost:5000/has thenative.codeclient registered with these exact settings:- Client type set to
Native(WPF is a desktop/native app, so this is critical) AllowedGrantTypesincludesauthorization_codeandrefresh_token(for token refresh later)AllowedScopesexplicitly listsopenid profile email fiver_auth_apiAllowedRedirectUrisprecisely matcheshttp://127.0.0.1/sample-wpf-app(avoid mixinglocalhostand127.0.0.1—consistency prevents callback failures)AllowOfflineAccess = true(required to get a refresh token for silent token renewal)
- Client type set to
Response Mode & Flow
Your choice ofFormPostresponse mode is perfect for embedded WPF browsers—it avoids issues with token data being lost during redirects. The Authorization Code flow is also the secure, recommended choice for native apps (far better than Implicit flow).WpfEmbeddedBrowser Implementation
Make sure your custom browser class correctly captures the FormPost callback to extract the authorization code and state. Here's a reliable implementation snippet if yours needs tuning:public class WpfEmbeddedBrowser : IBrowser { public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default) { var authWindow = new Window { Width = 600, Height = 800, WindowStartupLocation = WindowStartupLocation.CenterOwner, Title = "Login to IdentityServer" }; var webView = new WebBrowser(); authWindow.Content = webView; var tcs = new TaskCompletionSource<BrowserResult>(); webView.DocumentCompleted += (s, e) => { // Capture FormPost data when redirecting to our callback URI if (webView.Url.AbsoluteUri.StartsWith(options.RedirectUri)) { var form = webView.Document.Forms[0]; var code = form.All["code"]?.GetAttribute("value"); var state = form.All["state"]?.GetAttribute("value"); tcs.SetResult(new BrowserResult { Response = $"{options.RedirectUri}?code={code}&state={state}", ResultType = BrowserResultType.Success }); authWindow.Close(); } }; webView.Navigate(options.StartUrl); authWindow.ShowDialog(); return await tcs.Task; } }
Your CallButtonAsync method needs to handle token retrieval, authorization headers, and token refresh for a smooth user experience. Here's the full implementation:
Step 1: Complete the Authentication Flow
First, finish the login process and store tokens securely:
// Initialize OidcClient (store this as a class-level variable for reuse) private OidcClient _oidcClient; private string _accessToken; private string _refreshToken; private async void LoginButton_Click(object sender, RoutedEventArgs e) { var options = new OidcClientOptions { Authority = "http://localhost:5000/", ClientId = "native.code", Scope = "openid profile email fiver_auth_api", RedirectUri = "http://127.0.0.1/sample-wpf-app", ResponseMode = OidcClientOptions.AuthorizeResponseMode.FormPost, Flow = OidcClientOptions.AuthenticationFlow.AuthorizationCode, Browser = new WpfEmbeddedBrowser() }; _oidcClient = new OidcClient(options); var loginResult = await _oidcClient.LoginAsync(new LoginRequest()); if (loginResult.IsError) { MessageBox.Show($"Login failed: {loginResult.Error}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); return; } // Store tokens securely (use Windows Credential Manager for production!) _accessToken = loginResult.AccessToken; _refreshToken = loginResult.RefreshToken; }
Step 2: Refine the API Call Method
Now, update your API call to handle authorization, errors, and token refresh:
private async void CallApiButton_Click(object sender, RoutedEventArgs e) { await CallButtonAsync(); } private async Task CallButtonAsync() { if (string.IsNullOrEmpty(_accessToken)) { MessageBox.Show("Please log in first!", "Info", MessageBoxButton.OK, MessageBoxImage.Information); return; } using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); try { var response = await httpClient.GetAsync("http://your-api-url/api/protected-endpoint"); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(); MessageBox.Show($"API Response:\n{content}", "Success", MessageBoxButton.OK, MessageBoxImage.Information); } else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) { // Attempt to refresh the access token var refreshResult = await _oidcClient.RefreshTokenAsync(_refreshToken); if (!refreshResult.IsError) { _accessToken = refreshResult.AccessToken; _refreshToken = refreshResult.RefreshToken; // Retry the API call with the new token httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _accessToken); var retryResponse = await httpClient.GetAsync("http://your-api-url/api/protected-endpoint"); if (retryResponse.IsSuccessStatusCode) { var retryContent = await retryResponse.Content.ReadAsStringAsync(); MessageBox.Show($"API Response (after token refresh):\n{retryContent}", "Success", MessageBoxButton.OK, MessageBoxImage.Information); } else { MessageBox.Show("Token refresh succeeded but API call still failed.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } else { MessageBox.Show("Session expired. Please log in again.", "Info", MessageBoxButton.OK, MessageBoxImage.Information); // Trigger login flow here } } else { MessageBox.Show($"API call failed: {response.StatusCode}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } catch (Exception ex) { MessageBox.Show($"Error calling API: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } }
Secure Token Storage
Never store tokens in plaintext files or unprotected memory. For WPF apps, useWindows.Security.Credentials.PasswordVaultto store tokens securely in the Windows Credential Manager.Web API Token Validation
Ensure your Web API is configured to validate IdentityServer4 tokens. Add this to your API'sStartup.cs:services.AddAuthentication("Bearer") .AddJwtBearer("Bearer", options => { options.Authority = "http://localhost:5000/"; options.TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false // Set to true if your API has a specific audience }; }); // Enable authorization middleware app.UseAuthentication(); app.UseAuthorization();Scope-Based Authorization
Restrict API access to only clients with thefiver_auth_apiscope. Add a policy in your API'sStartup.cs:services.AddAuthorization(options => { options.AddPolicy("RequireFiverAuthApiScope", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", "fiver_auth_api"); }); });Then apply it to your API endpoints:
[Authorize(Policy = "RequireFiverAuthApiScope")] [ApiController] [Route("api/[controller]")] public class ProtectedController : ControllerBase { // Your endpoint logic here }
内容的提问来源于stack exchange,提问作者Alexandra Damaschin

