using System.Net.Http.Json; using System.Text.Json; namespace DysonNetwork.Sphere.Auth.OpenId; public class GitHubOidcService : OidcService { public GitHubOidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db) : base(configuration, httpClientFactory, db) { } public override string ProviderName => "GitHub"; protected override string DiscoveryEndpoint => ""; // GitHub doesn't have a standard OIDC discovery endpoint protected override string ConfigSectionName => "GitHub"; public override string GetAuthorizationUrl(string state, string nonce) { var config = GetProviderConfig(); var queryParams = new Dictionary { { "client_id", config.ClientId }, { "redirect_uri", config.RedirectUri }, { "scope", "user:email" }, { "state", state }, }; var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); return $"https://github.com/login/oauth/authorize?{queryString}"; } public override async Task ProcessCallbackAsync(OidcCallbackData callbackData) { var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code); if (tokenResponse?.AccessToken == null) { throw new InvalidOperationException("Failed to obtain access token from GitHub"); } var userInfo = await GetUserInfoAsync(tokenResponse.AccessToken); userInfo.AccessToken = tokenResponse.AccessToken; userInfo.RefreshToken = tokenResponse.RefreshToken; return userInfo; } protected override async Task ExchangeCodeForTokensAsync(string code, string? codeVerifier = null) { var config = GetProviderConfig(); var client = _httpClientFactory.CreateClient(); var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://github.com/login/oauth/access_token") { Content = new FormUrlEncodedContent(new Dictionary { { "client_id", config.ClientId }, { "client_secret", config.ClientSecret }, { "code", code }, { "redirect_uri", config.RedirectUri }, }) }; tokenRequest.Headers.Add("Accept", "application/json"); var response = await client.SendAsync(tokenRequest); response.EnsureSuccessStatusCode(); return await response.Content.ReadFromJsonAsync(); } private async Task GetUserInfoAsync(string accessToken) { var client = _httpClientFactory.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user"); request.Headers.Add("Authorization", $"Bearer {accessToken}"); request.Headers.Add("User-Agent", "DysonNetwork.Sphere"); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); var githubUser = JsonDocument.Parse(json).RootElement; var email = githubUser.TryGetProperty("email", out var emailElement) ? emailElement.GetString() : null; if (string.IsNullOrEmpty(email)) { email = await GetPrimaryEmailAsync(accessToken); } return new OidcUserInfo { UserId = githubUser.GetProperty("id").GetInt64().ToString(), Email = email, DisplayName = githubUser.TryGetProperty("name", out var nameElement) ? nameElement.GetString() ?? "" : "", PreferredUsername = githubUser.GetProperty("login").GetString() ?? "", ProfilePictureUrl = githubUser.TryGetProperty("avatar_url", out var avatarElement) ? avatarElement.GetString() ?? "" : "", Provider = ProviderName }; } private async Task GetPrimaryEmailAsync(string accessToken) { var client = _httpClientFactory.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/user/emails"); request.Headers.Add("Authorization", $"Bearer {accessToken}"); request.Headers.Add("User-Agent", "DysonNetwork.Sphere"); var response = await client.SendAsync(request); if (!response.IsSuccessStatusCode) return null; var emails = await response.Content.ReadFromJsonAsync>(); return emails?.FirstOrDefault(e => e.Primary)?.Email; } private class GitHubEmail { public string Email { get; set; } = ""; public bool Primary { get; set; } public bool Verified { get; set; } } }