From 47caff569d57c0ff0c572c3c9831c009c7b0096e Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Mon, 16 Jun 2025 23:10:54 +0800 Subject: [PATCH] :sparkles: Add microsoft OIDC --- .../Auth/OpenId/MicrosoftOidcService.cs | 113 ++++++++++++++++++ .../Auth/OpenId/OidcService.cs | 2 +- DysonNetwork.Sphere/Program.cs | 1 + DysonNetwork.Sphere/appsettings.json | 5 + 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 DysonNetwork.Sphere/Auth/OpenId/MicrosoftOidcService.cs diff --git a/DysonNetwork.Sphere/Auth/OpenId/MicrosoftOidcService.cs b/DysonNetwork.Sphere/Auth/OpenId/MicrosoftOidcService.cs new file mode 100644 index 0000000..c3b0789 --- /dev/null +++ b/DysonNetwork.Sphere/Auth/OpenId/MicrosoftOidcService.cs @@ -0,0 +1,113 @@ +using System.Net.Http.Json; +using System.Text.Json; + +namespace DysonNetwork.Sphere.Auth.OpenId; + +public class MicrosoftOidcService : OidcService +{ + public MicrosoftOidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db) + : base(configuration, httpClientFactory, db) + { + } + + public override string ProviderName => "Microsoft"; + + protected override string DiscoveryEndpoint => _configuration[$"Oidc:{ConfigSectionName}:DiscoveryEndpoint"] ?? throw new InvalidOperationException("Microsoft OIDC discovery endpoint is not configured."); + + protected override string ConfigSectionName => "Microsoft"; + + public override string GetAuthorizationUrl(string state, string nonce) + { + var config = GetProviderConfig(); + var discoveryDocument = GetDiscoveryDocumentAsync().GetAwaiter().GetResult(); + if (discoveryDocument?.AuthorizationEndpoint == null) + throw new InvalidOperationException("Authorization endpoint not found in discovery document."); + + var queryParams = new Dictionary + { + { "client_id", config.ClientId }, + { "response_type", "code" }, + { "redirect_uri", config.RedirectUri }, + { "response_mode", "query" }, + { "scope", "openid profile email" }, + { "state", state }, + { "nonce", nonce }, + }; + + var queryString = string.Join("&", queryParams.Select(p => $"{p.Key}={Uri.EscapeDataString(p.Value)}")); + return $"{discoveryDocument.AuthorizationEndpoint}?{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 Microsoft"); + } + + 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 discoveryDocument = await GetDiscoveryDocumentAsync(); + if (discoveryDocument?.TokenEndpoint == null) + { + throw new InvalidOperationException("Token endpoint not found in discovery document."); + } + + var client = _httpClientFactory.CreateClient(); + + var tokenRequest = new HttpRequestMessage(HttpMethod.Post, discoveryDocument.TokenEndpoint) + { + Content = new FormUrlEncodedContent(new Dictionary + { + { "client_id", config.ClientId }, + { "scope", "openid profile email" }, + { "code", code }, + { "redirect_uri", config.RedirectUri }, + { "grant_type", "authorization_code" }, + { "client_secret", config.ClientSecret }, + }) + }; + + var response = await client.SendAsync(tokenRequest); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(); + } + + private async Task GetUserInfoAsync(string accessToken) + { + var discoveryDocument = await GetDiscoveryDocumentAsync(); + if (discoveryDocument?.UserinfoEndpoint == null) + throw new InvalidOperationException("Userinfo endpoint not found in discovery document."); + + var client = _httpClientFactory.CreateClient(); + var request = new HttpRequestMessage(HttpMethod.Get, discoveryDocument.UserinfoEndpoint); + request.Headers.Add("Authorization", $"Bearer {accessToken}"); + + var response = await client.SendAsync(request); + response.EnsureSuccessStatusCode(); + + var json = await response.Content.ReadAsStringAsync(); + var microsoftUser = JsonDocument.Parse(json).RootElement; + + return new OidcUserInfo + { + UserId = microsoftUser.GetProperty("sub").GetString() ?? "", + Email = microsoftUser.TryGetProperty("email", out var emailElement) ? emailElement.GetString() : null, + DisplayName = microsoftUser.TryGetProperty("name", out var nameElement) ? nameElement.GetString() ?? "" : "", + PreferredUsername = microsoftUser.TryGetProperty("preferred_username", out var preferredUsernameElement) ? preferredUsernameElement.GetString() ?? "" : "", + ProfilePictureUrl = microsoftUser.TryGetProperty("picture", out var pictureElement) ? pictureElement.GetString() ?? "" : "", + Provider = ProviderName + }; + } +} diff --git a/DysonNetwork.Sphere/Auth/OpenId/OidcService.cs b/DysonNetwork.Sphere/Auth/OpenId/OidcService.cs index 2b058f3..08571f7 100644 --- a/DysonNetwork.Sphere/Auth/OpenId/OidcService.cs +++ b/DysonNetwork.Sphere/Auth/OpenId/OidcService.cs @@ -58,7 +58,7 @@ public abstract class OidcService { ClientId = _configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "", ClientSecret = _configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "", - RedirectUri = _configuration["BaseUrl"] + "/auth/callback/" + ProviderName + RedirectUri = _configuration["BaseUrl"] + "/auth/callback/" + ProviderName.ToLower() }; } diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 3630bee..e1515d7 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -94,6 +94,7 @@ builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddControllers().AddJsonOptions(options => { diff --git a/DysonNetwork.Sphere/appsettings.json b/DysonNetwork.Sphere/appsettings.json index d38d6df..88cdb29 100644 --- a/DysonNetwork.Sphere/appsettings.json +++ b/DysonNetwork.Sphere/appsettings.json @@ -94,6 +94,11 @@ "TeamId": "W7HPZ53V6B", "KeyId": "B668YP4KBG", "PrivateKeyPath": "./Keys/Solarpass.p8" + }, + "Microsoft": { + "ClientId": "YOUR_MICROSOFT_CLIENT_ID", + "ClientSecret": "YOUR_MICROSOFT_CLIENT_SECRET", + "DiscoveryEndpoint": "YOUR_MICROSOFT_DISCOVERY_ENDPOINT" } }, "KnownProxies": [