diff --git a/src/Infrastructure/OAuth/AuthenticationOAuthOptions.cs b/src/Infrastructure/OAuth/AuthenticationOAuthOptions.cs new file mode 100644 index 0000000..1ee21b2 --- /dev/null +++ b/src/Infrastructure/OAuth/AuthenticationOAuthOptions.cs @@ -0,0 +1,15 @@ +using Microsoft.AspNetCore.Authentication.OAuth; + +namespace Infrastructure.OAuth +{ + public class AuthenticationOAuthOptions : OAuthOptions + { + public virtual string RedirectUri { get; set; } + + public virtual string OpenId { get; set; } = "urn:openid"; + + public virtual string AccessToken { get; set; } = "urn:access_token"; + + public virtual string Name { get; set; } = "urn:name"; + } +} \ No newline at end of file diff --git a/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationExtensions.cs b/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationExtensions.cs new file mode 100644 index 0000000..bdf6248 --- /dev/null +++ b/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Authentication; + +namespace Infrastructure.OAuth.Gitee +{ + public static class GiteeAuthenticationExtensions + { + public const string AuthenticationSchemeName = "Gitee"; + + public static AuthenticationBuilder AddGiteeAuthentication(this AuthenticationBuilder builder) + { + return builder.AddGiteeAuthentication(AuthenticationSchemeName, options => { }); + } + + public static AuthenticationBuilder AddGiteeAuthentication(this AuthenticationBuilder builder, + Action configureOptions) + { + return builder.AddGiteeAuthentication(AuthenticationSchemeName, configureOptions); + } + + public static AuthenticationBuilder AddGiteeAuthentication(this AuthenticationBuilder builder, string scheme, + Action configureOptions) + { + return builder.AddGiteeAuthentication(scheme, AuthenticationSchemeName, configureOptions); + } + + public static AuthenticationBuilder AddGiteeAuthentication(this AuthenticationBuilder builder, string scheme, + string displayName, + Action configureOptions) + { + return builder.AddScheme(scheme, displayName, + configureOptions); + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationHandler.cs b/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationHandler.cs new file mode 100644 index 0000000..b26f232 --- /dev/null +++ b/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationHandler.cs @@ -0,0 +1,48 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; + +namespace Infrastructure.OAuth.Gitee +{ + public class GiteeAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + IHttpClientFactory httpClientFactory, + UrlEncoder encoder) + : OAuthenticationHandlerBase(options, logger, httpClientFactory, encoder) + { + protected override string AuthenticationSchemeName { get; set; } = + GiteeAuthenticationExtensions.AuthenticationSchemeName; + + protected override async Task> GenerateClaimsByCode(string code) + { + var tokenQueryPairs = new List>() + { + new("grant_type", "authorization_code"), + new("client_id", Options.ClientId), + new("client_secret", Options.ClientSecret), + new("redirect_uri", Options.RedirectUri), + new("code", code) + }; + var giteeToken = + await SeedHttpMessageAsync(Options.TokenEndpoint, tokenQueryPairs, HttpMethod.Post); + + var userInfoQueryPairs = new List>() + { + new("access_token", giteeToken.AccessToken), + }; + + var userInfo = + await SeedHttpMessageAsync(Options.UserInformationEndpoint, userInfoQueryPairs); + var claims = new List() + { + new(Options.AvatarUrl, userInfo.AvatarUrl), + new(Options.Url, userInfo.Url), + + new(Options.OpenId, userInfo.Id.ToString()), + new(Options.Name, userInfo.Name), + new(Options.AccessToken, giteeToken.AccessToken) + }; + return claims; + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationOptions.cs b/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationOptions.cs new file mode 100644 index 0000000..ced8b33 --- /dev/null +++ b/src/Infrastructure/OAuth/Gitee/GiteeAuthenticationOptions.cs @@ -0,0 +1,32 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; + +namespace Infrastructure.OAuth.Gitee +{ + public class GiteeAuthenticationOptions : AuthenticationOAuthOptions + { + public string UserEmailsEndpoint { get; } = "https://gitee.com/api/v5/emails"; + + public string Url { get; } = "urn:gitee:url"; + + public string AvatarUrl { get; } = "urn:gitee:avatarUrl"; + + public GiteeAuthenticationOptions() + { + ClaimsIssuer = GiteeAuthenticationExtensions.AuthenticationSchemeName; + CallbackPath = "/signin-gitee"; + AuthorizationEndpoint = "https://gitee.com/oauth/authorize"; + TokenEndpoint = "https://gitee.com/oauth/token"; + UserInformationEndpoint = "https://gitee.com/api/v5/user"; + + Scope.Add("user_info"); + Scope.Add("emails"); + + ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id"); + ClaimActions.MapJsonKey(ClaimTypes.Actor, "login"); + ClaimActions.MapJsonKey(ClaimTypes.Email, "email"); + ClaimActions.MapJsonKey(ClaimTypes.Name, "name"); + ClaimActions.MapJsonKey(Url, "url"); + } + } +} \ No newline at end of file diff --git a/src/Infrastructure/OAuth/Gitee/GiteeToken.cs b/src/Infrastructure/OAuth/Gitee/GiteeToken.cs new file mode 100644 index 0000000..1a7854b --- /dev/null +++ b/src/Infrastructure/OAuth/Gitee/GiteeToken.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace Infrastructure.OAuth.Gitee +{ + public class GiteeToken + { + [JsonProperty("access_token")] + public string AccessToken { get; set; } + + [JsonProperty("token_type")] + public string TokenType { get; set; } + + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + + [JsonProperty("refresh_token")] + public string RefreshToken { get; set; } + + [JsonProperty("scope")] + public string Scope { get; set; } + + [JsonProperty("created_at")] + public long CreatedAt { get; set; } + } +} \ No newline at end of file diff --git a/src/Infrastructure/OAuth/Gitee/GiteeUserInfo.cs b/src/Infrastructure/OAuth/Gitee/GiteeUserInfo.cs new file mode 100644 index 0000000..76b2c70 --- /dev/null +++ b/src/Infrastructure/OAuth/Gitee/GiteeUserInfo.cs @@ -0,0 +1,25 @@ +using Newtonsoft.Json; + +namespace Infrastructure.OAuth.Gitee +{ + public class GiteeUserInfo() + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("login")] + public string Login { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("avatar_url")] + public string AvatarUrl { get; set; } + + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("email")] + public string Email { get; set; } + } +} \ No newline at end of file diff --git a/src/Infrastructure/OAuth/OAuthenticationHandlerBase.cs b/src/Infrastructure/OAuth/OAuthenticationHandlerBase.cs new file mode 100644 index 0000000..f19eabf --- /dev/null +++ b/src/Infrastructure/OAuth/OAuthenticationHandlerBase.cs @@ -0,0 +1,70 @@ +using System.Security.Claims; +using System.Text.Encodings.Web; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.WebUtilities; +using Newtonsoft.Json; + +namespace Infrastructure.OAuth; + +public abstract class OAuthenticationHandlerBase( + IOptionsMonitor options, + ILoggerFactory logger, + IHttpClientFactory httpClientFactory, + UrlEncoder encoder) + : AuthenticationHandler(options, logger, encoder) + where TOptions : AuthenticationSchemeOptions, new() +{ + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(); + + protected abstract string AuthenticationSchemeName { get; set; } + + protected abstract Task> GenerateClaimsByCode(string code); + + protected override async Task HandleAuthenticateAsync() + { + if (!Context.Request.Query.ContainsKey("code")) + { + return AuthenticateResult.Fail("query parameter code is missing"); + } + + var code = Context.Request.Query["code"].ToString(); + try + { + var claims = await GenerateClaimsByCode(code); + var claimsIdentity = new ClaimsIdentity(claims.ToArray(), AuthenticationSchemeName); + var ticket = new AuthenticationTicket(new ClaimsPrincipal(claimsIdentity), AuthenticationSchemeName); + return AuthenticateResult.Success(ticket); + } + catch (Exception e) + { + return AuthenticateResult.Fail(e.Message); + } + } + + protected async Task SeedHttpMessageAsync(string url, IEnumerable> query, + HttpMethod? httpMethod = null) + { + httpMethod = httpMethod ?? HttpMethod.Get; + + var queryUrl = QueryHelpers.AddQueryString(url, query); + + var response = default(HttpResponseMessage); + if (httpMethod == HttpMethod.Get) + { + response = await _httpClient.GetAsync(queryUrl); + } + else if (httpMethod == HttpMethod.Post) + { + response = await _httpClient.PostAsync(queryUrl, null); + } + + var content = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + throw new HttpRequestException($"授权服务器请求错误,请求地址:{queryUrl},错误信息:{content}"); + } + + return JsonConvert.DeserializeObject(content); + } +} \ No newline at end of file