📄 src/Integrations/Vasttrafik/OAuth2Handler.cs
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;

namespace MMirror.Integrations.Vasttrafik;

internal sealed class OAuth2Handler(IOptions<ClientOptions> options, IMemoryCache memoryCache, HttpClient httpClient)
    : DelegatingHandler
{
    private readonly string id = options.Value.ClientId;
    private readonly string secret = options.Value.ClientSecret;

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken
    )
    {
        request.Headers.Authorization = await GetAuthorization();

        return await base.SendAsync(request, cancellationToken);
    }

    private async Task<AuthenticationHeaderValue> GetAuthorization() =>
        (await memoryCache.GetOrCreateAsync("VasttrafikAuthorization", GetToken))!;

    private async Task<AuthenticationHeaderValue> GetToken(ICacheEntry cacheEntry)
    {
        var request = new HttpRequestMessage(HttpMethod.Post, "/token")
        {
            Headers =
            {
                Authorization = new("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes($"{id}:{secret}"))),
            },
            Content = new FormUrlEncodedContent([new("grant_type", "client_credentials")]),
        };
        var authResponse = await httpClient.SendAsync(request);
        var token =
            await authResponse.Content.ReadFromJsonAsync(OAuth2JsonSerializerContext.Default.OAuth2TokenResponse)
            ?? throw new Exception("Parsed literal null from Vasttrafik token endpoint.");

        var jwtHandler = new JwtSecurityTokenHandler();
        var jwt = jwtHandler.ReadJwtToken(token.AccessToken);

        cacheEntry.AbsoluteExpiration = jwt.ValidTo;

        return new("Bearer", token.AccessToken);
    }
}

[JsonSerializable(typeof(OAuth2TokenResponse))]
internal sealed partial class OAuth2JsonSerializerContext : JsonSerializerContext;

internal sealed record OAuth2TokenResponse([property: JsonPropertyName("access_token")] string AccessToken);