using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.Json; using System.Threading.Tasks; using GB5Shared.DTO.SSO; using Microsoft.Extensions.Logging; namespace GB5Shared.SSO { public class OidcDiscovery { private readonly IHttpClientFactory _httpFactory; private readonly OidcOptionsDTO _opts; private readonly ILogger _logger; private readonly SemaphoreSlim _lock = new(1, 1); private JsonDocument? _metadata; private DateTime _fetchedAt = DateTime.MinValue; private readonly TimeSpan _cacheDuration = TimeSpan.FromMinutes(30); public OidcDiscovery(IHttpClientFactory httpFactory, OidcOptionsDTO opts, ILogger logger) { _httpFactory = httpFactory; _opts = opts; _logger = logger; } public async Task GetMetadataAsync(CancellationToken cancellation = default) { if (_metadata != null && DateTime.UtcNow - _fetchedAt < _cacheDuration) return _metadata; await _lock.WaitAsync(cancellation); try { if (_metadata != null && DateTime.UtcNow - _fetchedAt < _cacheDuration) return _metadata; using var client = _httpFactory.CreateClient("oidc"); var res = await client.GetAsync(_opts.MetadataUrl, cancellation); res.EnsureSuccessStatusCode(); var txt = await res.Content.ReadAsStringAsync(cancellation); _metadata = JsonDocument.Parse(txt); _fetchedAt = DateTime.UtcNow; _logger.LogInformation("Fetched OIDC metadata at {time}", _fetchedAt); return _metadata; } finally { _lock.Release(); } } public async Task GetStringAsync(string key, CancellationToken cancellation = default) { var md = await GetMetadataAsync(cancellation); if (!md.RootElement.TryGetProperty(key, out var prop)) throw new InvalidOperationException($"OIDC metadata missing '{key}'"); return prop.GetString()!; } public Task GetAuthorizeEndpointAsync(CancellationToken ct = default) => GetStringAsync("authorization_endpoint", ct); public Task GetTokenEndpointAsync(CancellationToken ct = default) => GetStringAsync("token_endpoint", ct); public Task GetUserInfoEndpointAsync(CancellationToken ct = default) => GetStringAsync("userinfo_endpoint", ct); public Task GetEndSessionEndpointAsync(CancellationToken ct = default) => GetStringAsync("end_session_endpoint", ct); public Task GetRevocationEndpointAsync(CancellationToken ct = default) { // optional return GetMetadataAsync(ct) .ContinueWith(t => { var md = t.Result; if (md.RootElement.TryGetProperty("revocation_endpoint", out var p)) return p.GetString(); return null; }, ct); } } }