Add afdian as OIDC provider

This commit is contained in:
LittleSheep 2025-06-22 02:22:19 +08:00
parent c6450757be
commit 6b0e5f919d
4 changed files with 119 additions and 1 deletions

View File

@ -97,7 +97,7 @@ public class AuthController(
.FirstOrDefaultAsync(); .FirstOrDefaultAsync();
return challenge is null return challenge is null
? NotFound("Auth challenge was not found.") ? NotFound("Auth challenge was not found.")
: challenge.Account.AuthFactors.Where(e => e.EnabledAt != null).ToList(); : challenge.Account.AuthFactors.Where(e => e is { EnabledAt: not null, Trustworthy: >= 1 }).ToList();
} }
[HttpPost("challenge/{id:guid}/factors/{factorId:guid}")] [HttpPost("challenge/{id:guid}/factors/{factorId:guid}")]

View File

@ -0,0 +1,115 @@
using System.Net.Http.Json;
using System.Text.Json;
using DysonNetwork.Sphere.Storage;
namespace DysonNetwork.Sphere.Auth.OpenId;
public class AfdianOidcService(
IConfiguration configuration,
IHttpClientFactory httpClientFactory,
AppDatabase db,
AuthService auth,
ICacheService cache
)
: OidcService(configuration, httpClientFactory, db, auth, cache)
{
public override string ProviderName => "Afdian";
protected override string DiscoveryEndpoint => ""; // Afdian doesn't have a standard OIDC discovery endpoint
protected override string ConfigSectionName => "Afdian";
public override string GetAuthorizationUrl(string state, string nonce)
{
var config = GetProviderConfig();
var queryParams = new Dictionary<string, string>
{
{ "client_id", config.ClientId },
{ "redirect_uri", config.RedirectUri },
{ "response_type", "code" },
{ "scope", "basic" },
{ "state", state },
};
var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}"));
return $"https://afdian.com/oauth2/authorize?{queryString}";
}
protected override Task<OidcDiscoveryDocument?> GetDiscoveryDocumentAsync()
{
return Task.FromResult(new OidcDiscoveryDocument
{
AuthorizationEndpoint = "https://afdian.com/oauth2/authorize",
TokenEndpoint = "https://afdian.com/api/oauth2/access_token",
UserinfoEndpoint = null,
JwksUri = null
})!;
}
public override async Task<OidcUserInfo> ProcessCallbackAsync(OidcCallbackData callbackData)
{
var tokenResponse = await ExchangeCodeForTokensAsync(callbackData.Code);
if (tokenResponse?.AccessToken == null)
{
throw new InvalidOperationException("Failed to obtain access token from Afdian");
}
var userInfo = await GetUserInfoAsync(tokenResponse.AccessToken);
userInfo.AccessToken = tokenResponse.AccessToken;
userInfo.RefreshToken = tokenResponse.RefreshToken;
return userInfo;
}
protected override async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code,
string? codeVerifier = null)
{
var config = GetProviderConfig();
var client = HttpClientFactory.CreateClient();
var content = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "client_id", config.ClientId },
{ "client_secret", config.ClientSecret },
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", config.RedirectUri },
});
var response = await client.PostAsync("https://afdian.com/api/oauth2/access_token", content);
response.EnsureSuccessStatusCode();
return await response.Content.ReadFromJsonAsync<OidcTokenResponse>();
}
private async Task<OidcUserInfo> GetUserInfoAsync(string accessToken)
{
var client = HttpClientFactory.CreateClient();
var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/api/users/@me");
request.Headers.Add("Authorization", $"Bearer {accessToken}");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var discordUser = JsonDocument.Parse(json).RootElement;
var userId = discordUser.GetProperty("id").GetString() ?? "";
var avatar = discordUser.TryGetProperty("avatar", out var avatarElement) ? avatarElement.GetString() : null;
return new OidcUserInfo
{
UserId = userId,
Email = (discordUser.TryGetProperty("email", out var emailElement) ? emailElement.GetString() : null) ?? "",
EmailVerified = discordUser.TryGetProperty("verified", out var verifiedElement) &&
verifiedElement.GetBoolean(),
DisplayName = (discordUser.TryGetProperty("global_name", out var globalNameElement)
? globalNameElement.GetString()
: null) ?? "",
PreferredUsername = discordUser.GetProperty("username").GetString() ?? "",
ProfilePictureUrl = !string.IsNullOrEmpty(avatar)
? $"https://cdn.discordapp.com/avatars/{userId}/{avatar}.png"
: "",
Provider = ProviderName
};
}
}

View File

@ -120,6 +120,7 @@ public class OidcController(
"microsoft" => serviceProvider.GetRequiredService<MicrosoftOidcService>(), "microsoft" => serviceProvider.GetRequiredService<MicrosoftOidcService>(),
"discord" => serviceProvider.GetRequiredService<DiscordOidcService>(), "discord" => serviceProvider.GetRequiredService<DiscordOidcService>(),
"github" => serviceProvider.GetRequiredService<GitHubOidcService>(), "github" => serviceProvider.GetRequiredService<GitHubOidcService>(),
"afdian" => serviceProvider.GetRequiredService<AfdianOidcService>(),
_ => throw new ArgumentException($"Unsupported provider: {provider}") _ => throw new ArgumentException($"Unsupported provider: {provider}")
}; };
} }

View File

@ -53,11 +53,13 @@ public static class ServiceCollectionExtensions
services.AddScoped<OidcService, GitHubOidcService>(); services.AddScoped<OidcService, GitHubOidcService>();
services.AddScoped<OidcService, MicrosoftOidcService>(); services.AddScoped<OidcService, MicrosoftOidcService>();
services.AddScoped<OidcService, DiscordOidcService>(); services.AddScoped<OidcService, DiscordOidcService>();
services.AddScoped<OidcService, AfdianOidcService>();
services.AddScoped<GoogleOidcService>(); services.AddScoped<GoogleOidcService>();
services.AddScoped<AppleOidcService>(); services.AddScoped<AppleOidcService>();
services.AddScoped<GitHubOidcService>(); services.AddScoped<GitHubOidcService>();
services.AddScoped<MicrosoftOidcService>(); services.AddScoped<MicrosoftOidcService>();
services.AddScoped<DiscordOidcService>(); services.AddScoped<DiscordOidcService>();
services.AddScoped<AfdianOidcService>();
services.AddControllers().AddJsonOptions(options => services.AddControllers().AddJsonOptions(options =>
{ {