diff --git a/IRaCIS.Core.Application/Service/MinimalApiService/OAuth/LogtoTokenResponse.cs b/IRaCIS.Core.Application/Service/MinimalApiService/OAuth/LogtoTokenResponse.cs index 51323d93d..d2f62c7f8 100644 --- a/IRaCIS.Core.Application/Service/MinimalApiService/OAuth/LogtoTokenResponse.cs +++ b/IRaCIS.Core.Application/Service/MinimalApiService/OAuth/LogtoTokenResponse.cs @@ -1,4 +1,5 @@ -using System.Text.Json.Serialization; +using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace IRaCIS.Core.Application.Service.OAuth; @@ -8,30 +9,35 @@ public class LogtoTokenResponse /// The access token issued by the Logto authorization server. /// [JsonPropertyName("access_token")] + [JsonProperty("access_token")] public string AccessToken { get; set; } = null!; /// /// The type of the token issued by the Logto authorization server. /// [JsonPropertyName("token_type")] + [JsonProperty("token_type")] public string TokenType { get; set; } = null!; /// /// The lifetime in seconds of the access token. /// [JsonPropertyName("expires_in")] + [JsonProperty("expires_in")] public int ExpiresIn { get; set; } /// /// The refresh token, which can be used to obtain new access tokens using the same authorization grant. /// [JsonPropertyName("refresh_token")] + [JsonProperty("refresh_token")] public string? RefreshToken { get; set; } = null!; /// /// The ID token, which can be used to verify the identity of the user. /// [JsonPropertyName("id_token")] + [JsonProperty("id_token")] public string? IdToken { get; set; } = null; } diff --git a/IRaCIS.Core.Application/Service/MinimalApiService/OAuthService.cs b/IRaCIS.Core.Application/Service/MinimalApiService/OAuthService.cs index db9dd5f96..921983ae9 100644 --- a/IRaCIS.Core.Application/Service/MinimalApiService/OAuthService.cs +++ b/IRaCIS.Core.Application/Service/MinimalApiService/OAuthService.cs @@ -1,11 +1,20 @@ -using IdentityModel.Client; +using Azure.Core; +using IdentityModel; +using IdentityModel.Client; using IRaCIS.Core.Application.Service.OAuth; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NPOI.SS.Formula.Functions; +using Org.BouncyCastle.Utilities.Net; using RestSharp; using System; using System.Collections.Generic; using System.Linq; using System.Net; +using System.Net.Http; +using System.Security.Cryptography; using System.Text; using System.Text.Json; using System.Threading.Tasks; @@ -16,6 +25,360 @@ namespace IRaCIS.Core.Application.Service { + + #region authorization_code 原生 PKCE + + public IResponseOutput TestPCKEOrgin() + { + // 1. 生成 code_verifier 和 code_challenge + string codeVerifier = "QMSBBxTQrpKPscvNNfmaQfmyk5Wd33GZS1FKSo3Shv8w-59vW1iTSlgAznYojkYv2DgR4XhTqySsBnDPq0"; + + + string codeChallenge = PkceUtil.GenerateCodeChallenge(codeVerifier); + + Console.WriteLine(codeVerifier); + + string clientId = "aj34vqrpvz8olsbxwtcog"; + string redirectUri = "http://localhost:6100/OAuth/TestPKCECallBack"; + string state = "123456"; + + // 构造请求的 URL + string authorizationUrl = $"https://logto.test.extimaging.com/oidc/auth" + + $"?client_id={clientId}" + + $"&redirect_uri={Uri.EscapeDataString(redirectUri)}" + + $"&response_type=code" + + $"&scope=openid profile email phone" + + $"&code_challenge={codeChallenge}" + + $"&code_challenge_method=S256" + + $"&state={state}"; + + Console.WriteLine("请将以下 URL 复制到浏览器中,完成登录后获取 code:"); + + Console.WriteLine(authorizationUrl); + + return ResponseOutput.Ok(); + + } + + + [AllowAnonymous] + [RoutePattern(HttpMethod = "Get")] + public async Task TestPKCECallBackAsync(string code) + { + string codeVerifier = "QMSBBxTQrpKPscvNNfmaQfmyk5Wd33GZS1FKSo3Shv8w-59vW1iTSlgAznYojkYv2DgR4XhTqySsBnDPq0"; + // OIDC 配置,替换为您的 OIDC 提供者的配置 + string tokenEndpoint = "https://logto.test.extimaging.com/oidc/token"; // 替换为实际 token 端点 + string clientId = "aj34vqrpvz8olsbxwtcog"; + string redirectUri = "http://localhost:6100/OAuth/TestPKCECallBack"; // 替换为前端的回调 URL + + var baseUrl = "https://logto.test.extimaging.com"; + var opts = new RestClientOptions(baseUrl); + using var client = new RestClient(opts); + + //https://blog.logto.io/troubleshoot-invalid-grant-error/ + var request = new RestRequest("oidc/token", Method.Post) + .AddHeader("Content-Type", "application/x-www-form-urlencoded"); + + request.AddParameter("grant_type", "authorization_code") + .AddParameter("code", code) + .AddParameter("redirect_uri", redirectUri) + .AddParameter("client_id", clientId) + .AddParameter("code_verifier", codeVerifier); // 使用 PKCE + + + // 发送请求并获取响应 + var response = await client.ExecuteAsync(request); + + if (response.StatusCode == HttpStatusCode.OK) + { + var tokenResponse = response.Data; + + Console.WriteLine(tokenResponse.ToJsonStr()); + + var userInfoRequest = new RestRequest($"oidc/me", Method.Get) + .AddHeader("Authorization", $"Bearer {tokenResponse.AccessToken}"); + + var userResponse = await client.ExecuteAsync(userInfoRequest); + + Console.WriteLine(userResponse.Content); + } + + return ResponseOutput.Ok(); + } + #endregion + + + #region authorization_code OidcClient PKCE + + [AllowAnonymous] + [RoutePattern(HttpMethod = "Get")] + public IResponseOutput TestPKCE() + { + // 1. 生成 code_verifier 和 code_challenge + string codeVerifier = "QMSBBxTQrpKPscvNNfmaQfmyk5Wd33GZS1FKSo3Shv8w-59vW1iTSlgAznYojkYv2DgR4XhTqySsBnDPq0"; + + + string codeChallenge = PkceUtil.GenerateCodeChallenge(codeVerifier); + + Console.WriteLine(codeVerifier); + + string clientId = "aj34vqrpvz8olsbxwtcog"; + string redirectUri = "http://localhost:6100/OAuth/TestOidcClientPKCECallBack"; + string state = "123456"; + + // 构造请求的 URL + string authorizationUrl = $"https://logto.test.extimaging.com/oidc/auth" + + $"?client_id={clientId}" + + $"&redirect_uri={Uri.EscapeDataString(redirectUri)}" + + $"&response_type=code" + + $"&scope=openid profile email phone" + + $"&code_challenge={codeChallenge}" + + $"&code_challenge_method=S256" + + $"&state={state}"; + + Console.WriteLine("请将以下 URL 复制到浏览器中,完成登录后获取 code:"); + + Console.WriteLine(authorizationUrl); + + return ResponseOutput.Ok(); + + } + + [AllowAnonymous] + [RoutePattern(HttpMethod = "Get")] + public async Task TestOidcClientPKCECallBackAsync(string code) + { + //使用IdentityModel.OidcClient 测试 + var client = new HttpClient(); + var disco = await client.GetDiscoveryDocumentAsync("https://logto.test.extimaging.com/oidc"); + if (disco.IsError) + { + Console.WriteLine(disco.Error); + } + + // OIDC 配置,替换为您的 OIDC 提供者的配置 + string clientId = "aj34vqrpvz8olsbxwtcog"; + string codeVerifier = "QMSBBxTQrpKPscvNNfmaQfmyk5Wd33GZS1FKSo3Shv8w-59vW1iTSlgAznYojkYv2DgR4XhTqySsBnDPq0"; + string redirectUri = "http://localhost:6100/OAuth/TestOidcClientPKCECallBack"; // 替换为前端的回调 URL + + var requestBody = new Dictionary + { + { "grant_type", "authorization_code" }, + { "code", code }, + { "redirect_uri", redirectUri }, + { "client_id", clientId }, + { "code_verifier", codeVerifier } // 使用 PKCE + }; + + var _httpClient = new HttpClient(); + var content = new FormUrlEncodedContent(requestBody); + // 发出 token 请求 + var response = await _httpClient.PostAsync(disco.TokenEndpoint, content); + + + if (response.IsSuccessStatusCode) + { + var responseBody = await response.Content.ReadAsStringAsync(); + + // 解析 JSON + var jsonObject = JObject.Parse(responseBody); + + // 格式化并输出 JSON + var formattedJson = jsonObject.ToString(Formatting.Indented); + Console.WriteLine(formattedJson); + + + var tokenResponse=JsonConvert.DeserializeObject(responseBody); + + Console.WriteLine(tokenResponse); + + } + else + { + var errorContent = await response.Content.ReadAsStringAsync(); + throw new Exception($"Error: {errorContent}"); + } + + #region 提示必须要Secret + //// 准备请求内容 + //var tokenRequest = new AuthorizationCodeTokenRequest + //{ + // Address = disco.TokenEndpoint, + // ClientId = clientId, + // Code = code, + // RedirectUri = redirectUri, + // GrantType = "authorization_code", + // CodeVerifier = codeVerifier + + //}; + + //var tokenResponse = await _httpClient.RequestTokenAsync(tokenRequest); + + //if (tokenResponse.HttpStatusCode == HttpStatusCode.OK) + //{ + // var apiClient = new HttpClient(); + // apiClient.SetBearerToken(tokenResponse.AccessToken); + + // var response = await apiClient.GetAsync(disco.UserInfoEndpoint); + // if (!response.IsSuccessStatusCode) + // { + // Console.WriteLine(response.StatusCode); + // Console.WriteLine(response.ReasonPhrase); + // } + // else + // { + // var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; + // Console.WriteLine(JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true })); + + // //获取刷新token + // var refreshClient = new HttpClient(); + // var refreshRequest = new RefreshTokenRequest + // { + // Address = disco.TokenEndpoint, + // ClientId = clientId, + // RefreshToken = tokenResponse.RefreshToken, + // }; + + // var refreshResponse = await refreshClient.RequestRefreshTokenAsync(refreshRequest); + + // if (refreshResponse.IsError) + // { + // Console.WriteLine($"Error: {refreshResponse.Error}"); + // } + // else + // { + // Console.WriteLine("获取刷新token 完成"); + + // Console.WriteLine("AccessToken:" + refreshResponse.AccessToken); + + // Console.WriteLine("RefreshToken:" + refreshResponse.RefreshToken); + // } + + // } + //} + #endregion + + + + + return ResponseOutput.Ok(); + } + + + #endregion + + + #region OIDC authorization_code with client_secret + + [AllowAnonymous] + public IResponseOutput TestOidcClientWithSecret() + { + + string clientId = "tl42rjin7obxtwqqgvkti"; + string redirectUri = "http://localhost:6100/OAuth/TestOidcClientCallBack"; + string state = "123456"; + + // 构造请求的 URL + string authorizationUrl = $"https://logto.test.extimaging.com/oidc/auth" + + $"?client_id={clientId}" + + $"&redirect_uri={Uri.EscapeDataString(redirectUri)}" + + $"&response_type=code" + + $"&scope=openid profile email phone" + + $"&state={state}"; + + Console.WriteLine(authorizationUrl); + + return ResponseOutput.Ok(); + + } + + + + [AllowAnonymous] + [RoutePattern(HttpMethod = "Get")] + public async Task TestOidcClientCallBackAsync(string code) + { + //使用IdentityModel.OidcClient 测试 + var client = new HttpClient(); + var disco = await client.GetDiscoveryDocumentAsync("https://logto.test.extimaging.com/oidc"); + if (disco.IsError) + { + Console.WriteLine(disco.Error); + } + + // OIDC 配置,替换为您的 OIDC 提供者的配置 + string clientId = "tl42rjin7obxtwqqgvkti"; + string clientSecret = "Pu9ig4rz44aLlxb0yKUaOiZaFk6Bcu51"; + string redirectUri = "http://localhost:6100/OAuth/TestOidcClientCallBack"; // 替换为前端的回调 URL + // 准备请求内容 + var tokenRequest = new AuthorizationCodeTokenRequest + { + Address = disco.TokenEndpoint, + ClientId = clientId, + ClientSecret = clientSecret, + Code = code, + RedirectUri = redirectUri, + GrantType = "authorization_code", + + }; + + var _httpClient = new HttpClient(); + // 发出 token 请求 + var tokenResponse = await _httpClient.RequestAuthorizationCodeTokenAsync(tokenRequest); + + + if (tokenResponse.HttpStatusCode == HttpStatusCode.OK) + { + var apiClient = new HttpClient(); + apiClient.SetBearerToken(tokenResponse.AccessToken); + + var response = await apiClient.GetAsync(disco.UserInfoEndpoint); + if (!response.IsSuccessStatusCode) + { + Console.WriteLine(response.StatusCode); + Console.WriteLine(response.ReasonPhrase); + } + else + { + var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; + Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true })); + + //获取刷新token + var refreshClient = new HttpClient(); + var refreshRequest = new RefreshTokenRequest + { + Address = disco.TokenEndpoint, + ClientId = clientId, + ClientSecret = clientSecret, + RefreshToken = tokenResponse.RefreshToken, + }; + + var refreshResponse = await refreshClient.RequestRefreshTokenAsync(refreshRequest); + + if (refreshResponse.IsError) + { + Console.WriteLine($"Error: {refreshResponse.Error}"); + } + else + { + Console.WriteLine("获取刷新token 完成"); + + Console.WriteLine("AccessToken:" + refreshResponse.AccessToken); + + Console.WriteLine("RefreshToken:" + refreshResponse.RefreshToken); + } + + } + } + + return ResponseOutput.Ok(); + } + + #endregion + + + #region 客户端凭证 + /// /// 测试客户端凭证代码 /// @@ -66,7 +429,7 @@ namespace IRaCIS.Core.Application.Service else { var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; - Console.WriteLine(JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true })); + Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true })); } } @@ -142,5 +505,37 @@ namespace IRaCIS.Core.Application.Service return ResponseOutput.Ok(); } + #endregion + + } + + public static class PkceUtil + { + // 生成 code_verifier + public static string GenerateCodeVerifier() + { + var bytes = new byte[64]; + using (var random = RandomNumberGenerator.Create()) + { + random.GetBytes(bytes); + return Convert.ToBase64String(bytes) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } + } + + // 生成 code_challenge + public static string GenerateCodeChallenge(string codeVerifier) + { + using (var sha256 = SHA256.Create()) + { + var bytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier)); + return Convert.ToBase64String(bytes) + .TrimEnd('=') + .Replace('+', '-') + .Replace('/', '_'); + } + } } }