.NET の OAuth クライアントは IdentityModel が定番だけど、あえて IdentityModel を使わず、HttpClient だけで OAuth2.0 の認可コードフローを通す。PKCE にも対応.
using System; using System.Collections.Generic; using System.Diagnostics; using System.Net.Http; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; namespace SampleWebApiClient { class Program { const string IdentityServerAddress = "IdentityServerのアドレス"; const string WebApiAddress = "Web API のアドレス"; const string ClientId = "クライアント ID"; const string Scope = "API スコープ"; const string RedirectUri = "リダイレクト URI"; static async Task Main(string[] args) { // PKCE(認証コード横取り対策) string codeVerifier; using (var rng = RNGCryptoServiceProvider.Create()) { var bytes = new byte[32]; rng.GetBytes(bytes); codeVerifier = Convert.ToBase64String(bytes) .Split('=')[0] .Replace('+', '-') .Replace('/', '_'); } string challenge; using (var sha256 = SHA256.Create()) { var challengeBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); challenge = Convert.ToBase64String(challengeBytes) .Split('=')[0] .Replace('+', '-') .Replace('/', '_'); } // Web ブラウザで認証ページを表示 var authorizeUrl = $"{IdentityServerAddress}/connect/authorize" + "?client_id=" + ClientId + "&response_type=code" + "&scope=" + Uri.EscapeUriString(Scope) + "&redirect_uri=" + Uri.EscapeUriString(RedirectUri) + "&code_challenge=" + challenge + "&code_challenge_method=S256"; ; var escapedUrl = authorizeUrl.Replace("&", "^&"); Process.Start(new ProcessStartInfo("cmd", $"/c start {escapedUrl}") { CreateNoWindow = true, }); // 認証コードを使ってアクセストークンを取得 Console.Write("AuthorizationCode:"); var code = Console.ReadLine(); var tokenClient = new HttpClient { BaseAddress = new Uri(IdentityServerAddress), }; var tokenResponse = await tokenClient.PostAsync( "/connect/token", new FormUrlEncodedContent(new Dictionary<string, string> { ["grant_type"] = "authorization_code", ["code"] = code, ["client_id"] = ClientId, ["redirect_uri"] = RedirectUri, ["code_verifier"] = codeVerifier, })); var tokenResponseBody = await tokenResponse.Content.ReadAsStringAsync(); if (!tokenResponse.IsSuccessStatusCode) { Console.WriteLine(tokenResponseBody); goto END; } var accessToken = JsonDocument.Parse(tokenResponseBody) .RootElement .GetProperty("access_token") .GetString(); // Web API を呼び出すクライアントは、 // アクセストークンを取得するクライアントとは別にしておく var apiClient = new HttpClient { BaseAddress = new Uri(WebApiAddress), }; // アクセストークンを設定 apiClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); // データを取得 var response = await apiClient.GetAsync( "/api/products"); response.EnsureSuccessStatusCode(); Console.WriteLine(await response.Content.ReadAsStringAsync()); END: Console.WriteLine("Enter で終了"); Console.ReadLine(); } } }