Compare commits
	
		
			2 Commits
		
	
	
		
			b27b6b8c1b
			...
			fe04b12561
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fe04b12561 | |||
| 47caff569d | 
| @@ -3,6 +3,7 @@ using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using System.Text.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| @@ -13,9 +14,10 @@ namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| public class AppleOidcService( | ||||
|     IConfiguration configuration, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     AppDatabase db | ||||
|     AppDatabase db, | ||||
|     ICacheService cache | ||||
| ) | ||||
|     : OidcService(configuration, httpClientFactory, db) | ||||
|     : OidcService(configuration, httpClientFactory, db, cache) | ||||
| { | ||||
|     private readonly IConfiguration _configuration = configuration; | ||||
|     private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; | ||||
|   | ||||
| @@ -1,15 +1,17 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
|  | ||||
| public class DiscordOidcService : OidcService | ||||
| public class DiscordOidcService( | ||||
|     IConfiguration configuration, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     AppDatabase db, | ||||
|     ICacheService cache | ||||
| ) | ||||
|     : OidcService(configuration, httpClientFactory, db, cache) | ||||
| { | ||||
|     public DiscordOidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db) | ||||
|         : base(configuration, httpClientFactory, db) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     public override string ProviderName => "Discord"; | ||||
|     protected override string DiscoveryEndpoint => ""; // Discord doesn't have a standard OIDC discovery endpoint | ||||
|     protected override string ConfigSectionName => "Discord"; | ||||
| @@ -46,10 +48,11 @@ public class DiscordOidcService : OidcService | ||||
|         return userInfo; | ||||
|     } | ||||
|  | ||||
|     protected override async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code, string? codeVerifier = null) | ||||
|     protected override async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code, | ||||
|         string? codeVerifier = null) | ||||
|     { | ||||
|         var config = GetProviderConfig(); | ||||
|         var client = _httpClientFactory.CreateClient(); | ||||
|         var client = HttpClientFactory.CreateClient(); | ||||
|  | ||||
|         var content = new FormUrlEncodedContent(new Dictionary<string, string> | ||||
|         { | ||||
| @@ -68,7 +71,7 @@ public class DiscordOidcService : OidcService | ||||
|  | ||||
|     private async Task<OidcUserInfo> GetUserInfoAsync(string accessToken) | ||||
|     { | ||||
|         var client = _httpClientFactory.CreateClient(); | ||||
|         var client = HttpClientFactory.CreateClient(); | ||||
|         var request = new HttpRequestMessage(HttpMethod.Get, "https://discord.com/api/users/@me"); | ||||
|         request.Headers.Add("Authorization", $"Bearer {accessToken}"); | ||||
|  | ||||
| @@ -85,10 +88,15 @@ public class DiscordOidcService : OidcService | ||||
|         { | ||||
|             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) ?? "", | ||||
|             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" : "", | ||||
|             ProfilePictureUrl = !string.IsNullOrEmpty(avatar) | ||||
|                 ? $"https://cdn.discordapp.com/avatars/{userId}/{avatar}.png" | ||||
|                 : "", | ||||
|             Provider = ProviderName | ||||
|         }; | ||||
|     } | ||||
|   | ||||
| @@ -1,15 +1,17 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
|  | ||||
| public class GitHubOidcService : OidcService | ||||
| public class GitHubOidcService( | ||||
|     IConfiguration configuration, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     AppDatabase db, | ||||
|     ICacheService cache | ||||
| ) | ||||
|     : OidcService(configuration, httpClientFactory, db, cache) | ||||
| { | ||||
|     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"; | ||||
| @@ -45,10 +47,11 @@ public class GitHubOidcService : OidcService | ||||
|         return userInfo; | ||||
|     } | ||||
|  | ||||
|     protected override async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code, string? codeVerifier = null) | ||||
|     protected override async Task<OidcTokenResponse?> ExchangeCodeForTokensAsync(string code, | ||||
|         string? codeVerifier = null) | ||||
|     { | ||||
|         var config = GetProviderConfig(); | ||||
|         var client = _httpClientFactory.CreateClient(); | ||||
|         var client = HttpClientFactory.CreateClient(); | ||||
|  | ||||
|         var tokenRequest = new HttpRequestMessage(HttpMethod.Post, "https://github.com/login/oauth/access_token") | ||||
|         { | ||||
| @@ -70,7 +73,7 @@ public class GitHubOidcService : OidcService | ||||
|  | ||||
|     private async Task<OidcUserInfo> GetUserInfoAsync(string accessToken) | ||||
|     { | ||||
|         var client = _httpClientFactory.CreateClient(); | ||||
|         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"); | ||||
| @@ -93,14 +96,16 @@ public class GitHubOidcService : OidcService | ||||
|             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() ?? "" : "", | ||||
|             ProfilePictureUrl = githubUser.TryGetProperty("avatar_url", out var avatarElement) | ||||
|                 ? avatarElement.GetString() ?? "" | ||||
|                 : "", | ||||
|             Provider = ProviderName | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     private async Task<string?> GetPrimaryEmailAsync(string accessToken) | ||||
|     { | ||||
|         var client = _httpClientFactory.CreateClient(); | ||||
|         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"); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Net.Http.Json; | ||||
| using System.Security.Cryptography; | ||||
| using System.Text; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| @@ -12,9 +13,10 @@ namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| public class GoogleOidcService( | ||||
|     IConfiguration configuration, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     AppDatabase db | ||||
|     AppDatabase db, | ||||
|     ICacheService cache | ||||
| ) | ||||
|     : OidcService(configuration, httpClientFactory, db) | ||||
|     : OidcService(configuration, httpClientFactory, db, cache) | ||||
| { | ||||
|     private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; | ||||
|  | ||||
| @@ -94,7 +96,8 @@ public class GoogleOidcService( | ||||
|                 client.DefaultRequestHeaders.Authorization = | ||||
|                     new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokenResponse.AccessToken); | ||||
|  | ||||
|                 var userInfoResponse = await client.GetFromJsonAsync<Dictionary<string, object>>(discoveryDocument.UserinfoEndpoint); | ||||
|                 var userInfoResponse = | ||||
|                     await client.GetFromJsonAsync<Dictionary<string, object>>(discoveryDocument.UserinfoEndpoint); | ||||
|  | ||||
|                 if (userInfoResponse != null) | ||||
|                 { | ||||
|   | ||||
							
								
								
									
										123
									
								
								DysonNetwork.Sphere/Auth/OpenId/MicrosoftOidcService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								DysonNetwork.Sphere/Auth/OpenId/MicrosoftOidcService.cs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
|  | ||||
| namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
|  | ||||
| public class MicrosoftOidcService( | ||||
|     IConfiguration configuration, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     AppDatabase db, | ||||
|     ICacheService cache | ||||
| ) | ||||
|     : OidcService(configuration, httpClientFactory, db, cache) | ||||
| { | ||||
|     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<string, string> | ||||
|         { | ||||
|             { "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<OidcUserInfo> 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<OidcTokenResponse?> 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<string, string> | ||||
|             { | ||||
|                 { "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<OidcTokenResponse>(); | ||||
|     } | ||||
|  | ||||
|     private async Task<OidcUserInfo> 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 | ||||
|         }; | ||||
|     } | ||||
| } | ||||
| @@ -2,6 +2,7 @@ using System.IdentityModel.Tokens.Jwt; | ||||
| using System.Net.Http.Json; | ||||
| using System.Text.Json.Serialization; | ||||
| using DysonNetwork.Sphere.Account; | ||||
| using DysonNetwork.Sphere.Storage; | ||||
| using Microsoft.EntityFrameworkCore; | ||||
| using Microsoft.IdentityModel.Tokens; | ||||
| using NodaTime; | ||||
| @@ -11,18 +12,16 @@ namespace DysonNetwork.Sphere.Auth.OpenId; | ||||
| /// <summary> | ||||
| /// Base service for OpenID Connect authentication providers | ||||
| /// </summary> | ||||
| public abstract class OidcService | ||||
| public abstract class OidcService( | ||||
|     IConfiguration configuration, | ||||
|     IHttpClientFactory httpClientFactory, | ||||
|     AppDatabase db, | ||||
|     ICacheService cache | ||||
| ) | ||||
| { | ||||
|     protected readonly IConfiguration _configuration; | ||||
|     protected readonly IHttpClientFactory _httpClientFactory; | ||||
|     protected readonly AppDatabase _db; | ||||
|  | ||||
|     protected OidcService(IConfiguration configuration, IHttpClientFactory httpClientFactory, AppDatabase db) | ||||
|     { | ||||
|         _configuration = configuration; | ||||
|         _httpClientFactory = httpClientFactory; | ||||
|         _db = db; | ||||
|     } | ||||
|     protected readonly IConfiguration Configuration = configuration; | ||||
|     protected readonly IHttpClientFactory HttpClientFactory = httpClientFactory; | ||||
|     protected readonly AppDatabase Db = db; | ||||
|  | ||||
|     /// <summary> | ||||
|     /// Gets the unique identifier for this provider | ||||
| @@ -56,9 +55,9 @@ public abstract class OidcService | ||||
|     { | ||||
|         return new ProviderConfiguration | ||||
|         { | ||||
|                         ClientId = _configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "", | ||||
|                         ClientSecret = _configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "", | ||||
|                         RedirectUri = _configuration["BaseUrl"] + "/auth/callback/" + ProviderName | ||||
|             ClientId = Configuration[$"Oidc:{ConfigSectionName}:ClientId"] ?? "", | ||||
|             ClientSecret = Configuration[$"Oidc:{ConfigSectionName}:ClientSecret"] ?? "", | ||||
|             RedirectUri = Configuration["BaseUrl"] + "/auth/callback/" + ProviderName.ToLower() | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| @@ -67,10 +66,28 @@ public abstract class OidcService | ||||
|     /// </summary> | ||||
|     protected async Task<OidcDiscoveryDocument?> GetDiscoveryDocumentAsync() | ||||
|     { | ||||
|         var client = _httpClientFactory.CreateClient(); | ||||
|         // Construct a cache key unique to the current provider: | ||||
|         var cacheKey = $"oidc-discovery:{ProviderName}"; | ||||
|  | ||||
|         // Try getting the discovery document from cache first: | ||||
|         var (found, cachedDoc) = await cache.GetAsyncWithStatus<OidcDiscoveryDocument>(cacheKey); | ||||
|         if (found && cachedDoc != null) | ||||
|         { | ||||
|             return cachedDoc; | ||||
|         } | ||||
|  | ||||
|         // If it's not cached, fetch from the actual discovery endpoint: | ||||
|         var client = HttpClientFactory.CreateClient(); | ||||
|         var response = await client.GetAsync(DiscoveryEndpoint); | ||||
|         response.EnsureSuccessStatusCode(); | ||||
|         return await response.Content.ReadFromJsonAsync<OidcDiscoveryDocument>(); | ||||
|         var doc = await response.Content.ReadFromJsonAsync<OidcDiscoveryDocument>(); | ||||
|  | ||||
|         // Store the discovery document in the cache for a while (e.g., 15 minutes): | ||||
|         if (doc is not null) | ||||
|             await cache.SetAsync(cacheKey, doc, TimeSpan.FromMinutes(15)); | ||||
|  | ||||
|         return doc; | ||||
|  | ||||
|     } | ||||
|  | ||||
|     /// <summary> | ||||
| @@ -87,7 +104,7 @@ public abstract class OidcService | ||||
|             throw new InvalidOperationException("Token endpoint not found in discovery document"); | ||||
|         } | ||||
|  | ||||
|         var client = _httpClientFactory.CreateClient(); | ||||
|         var client = HttpClientFactory.CreateClient(); | ||||
|         var content = new FormUrlEncodedContent(BuildTokenRequestParameters(code, config, codeVerifier)); | ||||
|  | ||||
|         var response = await client.PostAsync(discoveryDocument.TokenEndpoint, content); | ||||
| @@ -178,7 +195,7 @@ public abstract class OidcService | ||||
|     ) | ||||
|     { | ||||
|         // Create or update the account connection | ||||
|                 var connection = await _db.AccountConnections | ||||
|         var connection = await Db.AccountConnections | ||||
|             .FirstOrDefaultAsync(c => c.Provider == ProviderName && | ||||
|                                       c.ProvidedIdentifier == userInfo.UserId && | ||||
|                                       c.AccountId == account.Id | ||||
| @@ -195,7 +212,7 @@ public abstract class OidcService | ||||
|                 LastUsedAt = SystemClock.Instance.GetCurrentInstant(), | ||||
|                 AccountId = account.Id | ||||
|             }; | ||||
|                         await _db.AccountConnections.AddAsync(connection); | ||||
|             await Db.AccountConnections.AddAsync(connection); | ||||
|         } | ||||
|  | ||||
|         // Create a challenge that's already completed | ||||
| @@ -215,7 +232,7 @@ public abstract class OidcService | ||||
|             UserAgent = request.Request.Headers.UserAgent, | ||||
|         }; | ||||
|  | ||||
|                 await _db.AuthChallenges.AddAsync(challenge); | ||||
|         await Db.AuthChallenges.AddAsync(challenge); | ||||
|  | ||||
|         // Create a session | ||||
|         var session = new Session | ||||
| @@ -226,8 +243,8 @@ public abstract class OidcService | ||||
|             Challenge = challenge | ||||
|         }; | ||||
|  | ||||
|                 await _db.AuthSessions.AddAsync(session); | ||||
|                 await _db.SaveChangesAsync(); | ||||
|         await Db.AuthSessions.AddAsync(session); | ||||
|         await Db.SaveChangesAsync(); | ||||
|  | ||||
|         return session; | ||||
|     } | ||||
|   | ||||
| @@ -94,6 +94,7 @@ builder.Services.AddHttpClient(); | ||||
| builder.Services.AddScoped<OidcService, GoogleOidcService>(); | ||||
| builder.Services.AddScoped<OidcService, AppleOidcService>(); | ||||
| builder.Services.AddScoped<OidcService, GitHubOidcService>(); | ||||
| builder.Services.AddScoped<OidcService, MicrosoftOidcService>(); | ||||
| builder.Services.AddScoped<OidcService, DiscordOidcService>(); | ||||
| builder.Services.AddControllers().AddJsonOptions(options => | ||||
| { | ||||
|   | ||||
| @@ -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": [ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user