From cbbc703695f7fd3db76e6f3697dc4499fd5d203f Mon Sep 17 00:00:00 2001
From: Klaus Fischer <klaus.fischer@eloware.com>
Date: Thu, 30 Jun 2022 13:23:21 +0200
Subject: [PATCH] Implemented proxy support

---
 E2ETests/E2ETests.csproj             |  15 +--
 E2ETests/HelperMethods.cs            |  30 ++++++
 E2ETests/OAuthServiceTest.cs         |  21 +---
 E2ETests/ProxyTest.cs                |  80 +++++++++++++++
 Encryption/DefaultEncryptor.cs       |  24 -----
 Encryption/JoseEncryptor.cs          |  21 ----
 Encryption/RsaEncryption.cs          | 146 ---------------------------
 FitConnect/Client.cs                 |  26 ++++-
 FitConnect/DiContainer.cs            |   2 +
 FitConnect/FunctionalBaseClass.cs    | 110 --------------------
 RestService/RestCallService.cs       |  20 +++-
 Services/Interfaces/IOAuthService.cs |   3 +-
 Services/OAuthService.cs             |   9 +-
 13 files changed, 165 insertions(+), 342 deletions(-)
 create mode 100644 E2ETests/HelperMethods.cs
 create mode 100644 E2ETests/ProxyTest.cs
 delete mode 100644 Encryption/DefaultEncryptor.cs
 delete mode 100644 Encryption/JoseEncryptor.cs
 delete mode 100644 Encryption/RsaEncryption.cs
 delete mode 100644 FitConnect/FunctionalBaseClass.cs

diff --git a/E2ETests/E2ETests.csproj b/E2ETests/E2ETests.csproj
index b78e8852..67adfa9e 100644
--- a/E2ETests/E2ETests.csproj
+++ b/E2ETests/E2ETests.csproj
@@ -8,16 +8,17 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <PackageReference Include="FluentAssertions" Version="6.7.0"/>
-        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0"/>
-        <PackageReference Include="NUnit" Version="3.13.2"/>
-        <PackageReference Include="NUnit3TestAdapter" Version="4.0.0"/>
-        <PackageReference Include="coverlet.collector" Version="3.1.0"/>
+        <PackageReference Include="DotNet.Testcontainers" Version="1.6.0" />
+        <PackageReference Include="FluentAssertions" Version="6.7.0" />
+        <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
+        <PackageReference Include="NUnit" Version="3.13.2" />
+        <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />
+        <PackageReference Include="coverlet.collector" Version="3.1.0" />
     </ItemGroup>
 
     <ItemGroup>
-        <ProjectReference Include="..\FitConnect\FitConnect.csproj"/>
-        <ProjectReference Include="..\Services\Services.csproj"/>
+        <ProjectReference Include="..\FitConnect\FitConnect.csproj" />
+        <ProjectReference Include="..\Services\Services.csproj" />
     </ItemGroup>
 
 </Project>
diff --git a/E2ETests/HelperMethods.cs b/E2ETests/HelperMethods.cs
new file mode 100644
index 00000000..ba5a5d96
--- /dev/null
+++ b/E2ETests/HelperMethods.cs
@@ -0,0 +1,30 @@
+using System;
+using System.IO;
+using Newtonsoft.Json;
+
+namespace E2ETests;
+
+public static class HelperMethods {
+    public static (string id, string secret) GetSecrets() {
+        // relative to the project execution directory
+        const string secretFile = "../../../http-client.private.env.json";
+
+        if (!File.Exists(secretFile)) {
+            // If the secret file is not found, create it with the default values
+            // The file will be pretty in C#11, when """ is introduced
+            File.WriteAllText(secretFile, @"
+{
+    ""sender"": {
+        ""id"": ""00000000-0000-0000-0000-000000000000"",
+        ""secret"": ""0000000000000000000000000000000000000000000"",
+        ""scope"": ""send:region:DE""
+    }
+}");
+            throw new Exception("Please fill the secret.json file with your sender credentials");
+        }
+
+        var jsonContent = File.ReadAllText(secretFile);
+        var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent);
+        return (secret.sender.id, secret.sender.secret);
+    }
+}
diff --git a/E2ETests/OAuthServiceTest.cs b/E2ETests/OAuthServiceTest.cs
index b17b97d2..50c1314c 100644
--- a/E2ETests/OAuthServiceTest.cs
+++ b/E2ETests/OAuthServiceTest.cs
@@ -16,28 +16,9 @@ public class OAuthServiceTest {
 
     [OneTimeSetUp]
     public void OneTimeSetup() {
-        // relative to the project execution directory
-        const string secretFile = "../../../http-client.private.env.json";
-
-        if (!File.Exists(secretFile)) {
-            // If the secret file is not found, create it with the default values
-            // The file will be pretty in C#11, when """ is introduced
-            File.WriteAllText(secretFile, @"
-{
-    ""sender"": {
-        ""id"": ""00000000-0000-0000-0000-000000000000"",
-        ""secret"": ""0000000000000000000000000000000000000000000"",
-        ""scope"": ""send:region:DE""
+        (_clientId, _clientSecret) = HelperMethods.GetSecrets();
     }
-}");
-            throw new Exception("Please fill the secret.json file with your sender credentials");
-        }
 
-        var jsonContent = File.ReadAllText(secretFile);
-        var secret = JsonConvert.DeserializeObject<dynamic>(jsonContent);
-        _clientId = secret.sender.id;
-        _clientSecret = secret.sender.secret;
-    }
 
 
     [SetUp]
diff --git a/E2ETests/ProxyTest.cs b/E2ETests/ProxyTest.cs
new file mode 100644
index 00000000..03503db0
--- /dev/null
+++ b/E2ETests/ProxyTest.cs
@@ -0,0 +1,80 @@
+using System;
+using System.IO;
+using System.Net;
+using System.Reflection;
+using System.Threading;
+using NUnit.Framework;
+using DotNet.Testcontainers;
+using DotNet.Testcontainers.Builders;
+using DotNet.Testcontainers.Containers;
+using FitConnect;
+using FitConnect.Services;
+using FluentAssertions;
+
+namespace E2ETests;
+
+public class ProxyTest {
+    private TestcontainersContainer _container;
+    private Client _client;
+    private string _secret;
+    private string _id;
+
+    [OneTimeSetUp]
+    public void OneTimeSetup() {
+        var path = $"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}/proxy";
+        Console.WriteLine($"Creating directory: {path}");
+        Directory.CreateDirectory(path);
+        (_id, _secret) = HelperMethods.GetSecrets();
+
+        _container = new TestcontainersBuilder<TestcontainersContainer>()
+            .WithImage("ubuntu/squid")
+            .WithPortBinding("3128", "3128")
+            .WithBindMount(path, @"/var/log/squid")
+            .Build();
+    }
+
+    [SetUp]
+    public void Setup() {
+        _container.StartAsync().Wait();
+
+        _client =
+            new Client(FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development),
+                    _id,
+                    _secret)
+                .WithProxy("localhost", 3128);
+    }
+
+    [Test]
+    public void ContainerIsRunning() {
+        _container.Should().NotBeNull();
+        _container.State.Should().Be(TestcontainersState.Running);
+
+        var client = new WebClient() {
+            Proxy =
+                new WebProxy("http://localhost:3128")
+        };
+        client.DownloadString("https://www.fitko.de/").Should().NotBeNull();
+    }
+
+    [Test]
+    public void RequestOAuthToken() {
+        // Arrange
+        var testUrl = FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development)
+            .TokenUrl;
+
+        var oAuthService = new OAuthService(testUrl) {
+            Proxy = new WebProxy("http://localhost:3128")
+        };
+
+        // Act
+        var token = oAuthService.AuthenticateAsync(_id, _secret).Result;
+
+        // Assert
+        token.Should().NotBeNull();
+    }
+
+    [OneTimeTearDown]
+    public void OneTimeTearDown() {
+        _container.StopAsync().Wait();
+    }
+}
diff --git a/Encryption/DefaultEncryptor.cs b/Encryption/DefaultEncryptor.cs
deleted file mode 100644
index ada93cf9..00000000
--- a/Encryption/DefaultEncryptor.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Security.Cryptography;
-using System.Text;
-using Microsoft.IdentityModel.Tokens;
-
-namespace FitConnect.Encryption;
-
-public class DefaultEncryptor : IEncryptor {
-    public string Encrypt(string plain, string key, out object? passOver) {
-        var rsaKey = new JsonWebKey(key);
-        var rsa = RSA.Create();
-        passOver = rsa;
-
-        var cipher = rsa.Encrypt(Encoding.UTF8.GetBytes(plain), RSAEncryptionPadding.OaepSHA512);
-        return Convert.ToBase64String(cipher);
-    }
-
-    public string Decrypt(string cipher, string key, object? passover = null) {
-        var rsaKey = new JsonWebKey(key);
-        var rsa = passover as RSA ?? RSA.Create();
-
-        var plain = rsa.Decrypt(Convert.FromBase64String(cipher), RSAEncryptionPadding.OaepSHA512);
-        return Encoding.UTF8.GetString(plain);
-    }
-}
diff --git a/Encryption/JoseEncryptor.cs b/Encryption/JoseEncryptor.cs
deleted file mode 100644
index 38de0f4c..00000000
--- a/Encryption/JoseEncryptor.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using Jose;
-
-namespace JweTest;
-
-public class JoseEncryptor : IEncryptor {
-    public string Encrypt(string plain, string key, out object? passOver) {
-        passOver = null;
-        var jwk = Jwk.FromJson(key);
-        return JWE.Encrypt(plain,
-            new[] { new JweRecipient(JweAlgorithm.RSA_OAEP_256, jwk) },
-            JweEncryption.A256GCM);
-    }
-
-    public string Decrypt(string cipher, string key, object? passOver = null) {
-        var jwk = Jwk.FromJson(key);
-        var cipherBytes = Convert.FromBase64String(cipher);
-        var jwe = JWE.Decrypt(cipher, jwk);
-        return Convert.ToBase64String(jwe.Ciphertext);
-    }
-}
diff --git a/Encryption/RsaEncryption.cs b/Encryption/RsaEncryption.cs
deleted file mode 100644
index eed61bdc..00000000
--- a/Encryption/RsaEncryption.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-using System.IdentityModel.Tokens.Jwt;
-using System.Security.Claims;
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using Microsoft.Extensions.Logging;
-using Microsoft.IdentityModel.Tokens;
-
-namespace FitConnect.Encryption;
-
-public class RsaEncryption : IEncryption {
-    private readonly X509Certificate2? _certificate;
-    private readonly ILogger? _logger;
-    private readonly RSA _rsa;
-    private RSA? _privateKey;
-    private RSA? _publicKey;
-
-
-    public RsaEncryption(X509Certificate2? certificate, ILogger? logger) {
-        _logger = logger;
-        _rsa = RSA.Create(4096);
-
-        if (certificate != null) {
-            _certificate = certificate;
-            ImportCertificate(certificate);
-        }
-    }
-
-
-    public void ImportCertificate(X509Certificate2 cert) {
-        if (!CheckCertificate(cert)) throw new Exception("Invalid certificate");
-
-        _publicKey = cert.GetRSAPublicKey();
-
-        if ((_publicKey?.KeySize ?? 0) == 0)
-            throw new Exception("Invalid certificate, no public key");
-
-        if (cert.HasPrivateKey) _privateKey = cert.GetRSAPrivateKey();
-
-        if (_privateKey != null)
-            _logger?.LogInformation("Certificate with private key imported");
-        else
-            _logger?.LogInformation("Certificate has no private key");
-    }
-
-    /// <summary>
-    ///     Import a public key from a PEM file
-    /// </summary>
-    /// <param name="certificatePath"></param>
-    /// <param name="password">Password for the certificate</param>
-    /// <exception cref="ArgumentException"></exception>
-    /// <exception cref="Exception"></exception>
-    public void ImportCertificate(string certificatePath, string password) {
-        _logger?.LogInformation("Importing certificate {CertPath}", certificatePath);
-        var cert =
-            new X509Certificate2(certificatePath, password, X509KeyStorageFlags.MachineKeySet);
-
-        if (!CheckCertificate(cert)) throw new ArgumentException("Certificate is not valid");
-
-        var parameters = cert.PublicKey.GetRSAPublicKey()?.ExportParameters(true);
-        if (parameters == null) throw new Exception("Could not get public key from certificate");
-
-        _rsa.ImportParameters(parameters.Value);
-    }
-
-    public byte[] DecryptData(byte[] data) {
-        return
-            (_privateKey ?? _rsa).Decrypt(data, RSAEncryptionPadding.OaepSHA256);
-    }
-
-    public string DecryptData(string data) {
-        return Encoding.UTF8.GetString(DecryptData(Convert.FromBase64String(data)));
-    }
-
-    public byte[] ExportPublicKey() {
-        return _rsa.ExportRSAPublicKey();
-    }
-
-    public byte[] ExportPrivateKey() {
-        return _rsa.ExportRSAPrivateKey();
-    }
-
-    public byte[] EncryptData(byte[] data) {
-        return (_publicKey ?? _rsa).Encrypt(data, RSAEncryptionPadding.OaepSHA256);
-    }
-
-    public byte[] EncryptData(byte[] data, byte[] publicKey) {
-        _logger?.LogInformation(
-            "Encrypting data with public key: {}",
-            Convert.ToBase64String(_rsa.ExportRSAPublicKey()));
-
-        _rsa.ImportRSAPublicKey(publicKey, out var read);
-        return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256);
-    }
-
-    /// <summary>
-    ///     Creating a self signed certificate
-    /// </summary>
-    /// <param name="exportPath">Location for storing the certificate files</param>
-    /// <returns></returns>
-    /// <exception cref="Exception"></exception>
-    public static X509Certificate2 CreateSelfSignedCertificate(string? exportPath = null) {
-        var rsa = RSA.Create(4096);
-
-        var req = new CertificateRequest("c=DE, cn=fitconnect.de",
-            rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
-
-        var cert = req.CreateSelfSigned(DateTimeOffset.Now.AddSeconds(-5),
-            DateTimeOffset.Now.AddYears(5));
-
-        if (cert.GetRSAPublicKey() == null)
-            throw new Exception("Certificate does not contain a public key");
-
-        if (cert.GetRSAPrivateKey() == null)
-            throw new Exception("Certificate does not contain a private key");
-
-        // Export the certificate to a PEM file, just for
-        // additional extern testing
-        if (exportPath != null) ExportCertificateToFile(exportPath, cert);
-
-        return cert;
-    }
-
-    private static void ExportCertificateToFile(string exportPath, X509Certificate cert) {
-        // Create PFX (PKCS #12) with private key
-        File.WriteAllBytes($"{exportPath}/certificate.pfx",
-            cert.Export(X509ContentType.Pfx, ""));
-
-        // Create Base 64 encoded CER (public key only)
-        File.WriteAllText($"{exportPath}/certificate.cer",
-            "-----BEGIN CERTIFICATE-----\r\n"
-            + Convert.ToBase64String(cert.Export(X509ContentType.Cert, ""),
-                Base64FormattingOptions.InsertLineBreaks)
-            + "\r\n-----END CERTIFICATE-----");
-    }
-
-
-    /// <summary>
-    ///     Checking the certificate for validity
-    /// </summary>
-    /// <param name="cert">Certificate to check</param>
-    /// <returns>true for a fully approved certificate</returns>
-    private bool CheckCertificate(X509Certificate2 cert) {
-        return true;
-    }
-}
diff --git a/FitConnect/Client.cs b/FitConnect/Client.cs
index 5d45e246..ffad4ab2 100644
--- a/FitConnect/Client.cs
+++ b/FitConnect/Client.cs
@@ -1,3 +1,4 @@
+using System.Net;
 using System.Security.Cryptography.X509Certificates;
 using System.Text.Json;
 using Autofac;
@@ -13,12 +14,10 @@ namespace FitConnect;
 /// </summary>
 // ReSharper disable once UnusedType.Global
 public class Client {
-    private readonly X509Certificate2? _receiverCertificate;
-    private readonly X509Certificate2? _senderCertificate;
     private readonly FitConnectEndpoints _endpoints;
-    internal string ClientId;
+    internal readonly string ClientId;
 
-    internal string ClientSecret;
+    internal readonly string ClientSecret;
 
     private readonly ILogger? _logger;
 
@@ -53,6 +52,25 @@ public class Client {
         _logger = logger;
     }
 
+    /// <summary>
+    /// 
+    /// </summary>
+    /// <param name="host"></param>
+    /// <param name="port"></param>
+    /// <param name="username"></param>
+    /// <param name="password"></param>
+    /// <returns></returns>
+    public Client WithProxy(string host, int port, string? username = null,
+        string? password = null) {
+        var proxy = new WebProxy(host, port);
+        if (username != null && password != null) {
+            proxy.Credentials = new NetworkCredential(username, password);
+        }
+
+        DiContainer.WebProxy = proxy;
+        return this;
+    }
+
     public IFluentSubscriber GetSubscriber(string privateKeyEncryption, string privateKeySigning) {
         var deserializedRaw = JsonSerializer.Deserialize<string>(privateKeyEncryption);
         var deserialized = new X509Certificate2(deserializedRaw);
diff --git a/FitConnect/DiContainer.cs b/FitConnect/DiContainer.cs
index b3b99299..dd197139 100644
--- a/FitConnect/DiContainer.cs
+++ b/FitConnect/DiContainer.cs
@@ -1,3 +1,4 @@
+using System.Net;
 using System.Security.Cryptography.X509Certificates;
 using Autofac;
 using FitConnect.Services;
@@ -8,6 +9,7 @@ namespace FitConnect;
 
 public static class DiContainer {
     private static IContainer? _container;
+    public static IWebProxy? WebProxy { get; set; }
 
     public static void DisposeContainer() {
         _container?.Dispose();
diff --git a/FitConnect/FunctionalBaseClass.cs b/FitConnect/FunctionalBaseClass.cs
deleted file mode 100644
index 980ddc3b..00000000
--- a/FitConnect/FunctionalBaseClass.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System.Net.Http.Headers;
-using System.Net.Http.Json;
-using System.Security.Cryptography;
-using System.Text;
-using FitConnect.Models;
-using Microsoft.Extensions.Logging;
-
-namespace FitConnect;
-
-
-public abstract class FunctionalBaseClass {
-    protected readonly ILogger? logger;
-    private RSA _rsa;
-    public FitConnectEndpoints Endpoints { get; }
-
-    protected FunctionalBaseClass(ILogger? logger, FitConnectEndpoints? endpoints) {
-        Endpoints = endpoints ??
-                    FitConnectEndpoints.Create(FitConnectEndpoints.EndpointType.Development);
-
-        this.logger = logger;
-    }
-
-
-    /// <summary>
-    /// Requesting an OAuth token from the FitConnect API.
-    /// 
-    /// You can get the Client ID and Client Secret from the FitConnect Self Service portal
-    /// under https://portal.auth-testing.fit-connect.fitko.dev
-    /// </summary>
-    /// <param name="clientId">Your client Id</param>
-    /// <param name="clientSecret">Your client Secret</param>
-    /// <param name="scope">Scope if needed</param>
-    /// <returns></returns>
-    public async Task<OAuthAccessToken?> GetTokenAsync(string clientId, string clientSecret,
-        string? scope = null) {
-        var client = new HttpClient();
-        client.DefaultRequestHeaders.Accept.Add(
-            MediaTypeWithQualityHeaderValue.Parse("application/json"));
-
-        var requestContent = new Dictionary<string, string> {
-            { "grant_type", "client_credentials" },
-            { "client_id", clientId },
-            { "client_secret", clientSecret }
-        };
-
-        if (scope != null)
-            requestContent["scope"] = scope;
-
-        var content = new FormUrlEncodedContent(requestContent);
-
-        var request = new HttpRequestMessage(HttpMethod.Post, Endpoints.TokenUrl) {
-            Content = content,
-            Method = HttpMethod.Post
-        };
-
-        var response = await client.SendAsync(request);
-
-        return await response.Content.ReadFromJsonAsync<OAuthAccessToken>();
-    }
-
-    public Task<SecurityEventToken> GetSetDataAsync() {
-        throw new NotImplementedException();
-    }
-
-    public byte[] EncryptDataAsync(byte[] data,
-        byte[]? publicKey,
-        string? password,
-        int numberOfIterations) {
-        _rsa = RSA.Create(2048);
-        // _rsa.ImportRSAPublicKey(publicKey, out var read);
-
-        logger?.LogInformation(
-            "Encrypting data with public key: {}",
-            Convert.ToBase64String(_rsa.ExportRSAPublicKey()));
-
-        // var keyParams = new PbeParameters(
-        //     PbeEncryptionAlgorithm.Aes256Cbc, HashAlgorithmName.SHA256, numberOfIterations);
-        //
-        // var privateKey =
-        //     rsa.ExportEncryptedPkcs8PrivateKey(Encoding.UTF8.GetBytes(password), keyParams);
-        //
-        // logger?.LogInformation(
-        //     "Private key: {}", privateKey);
-
-        return _rsa.Encrypt(data, RSAEncryptionPadding.OaepSHA256);
-    }
-
-    public byte[] DecryptDataAsync(byte[] data, byte[] privateKey) {
-        return _rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256);
-    }
-
-
-    private async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) {
-        var client = new HttpClient();
-        client.DefaultRequestHeaders.Add("Accept", "application/json");
-        client.DefaultRequestHeaders.Add("Content-Type", "application/json");
-
-        var request = new HttpRequestMessage();
-        request.Method = method;
-        request.Content = new StringContent(body, Encoding.UTF8, "application/json");
-        request.RequestUri = uri;
-
-        var response = await client.SendAsync(request);
-
-        if (response.IsSuccessStatusCode)
-            return (await response.Content.ReadFromJsonAsync<T>());
-
-        throw new HttpRequestException("Error calling FitConnect API");
-    }
-}
diff --git a/RestService/RestCallService.cs b/RestService/RestCallService.cs
index 685b8017..77f8a699 100644
--- a/RestService/RestCallService.cs
+++ b/RestService/RestCallService.cs
@@ -1,25 +1,37 @@
+using System.Net;
 using System.Net.Http.Json;
 using System.Text;
 
 namespace FitConnect.RestService;
 
+public interface IRestCallService {
+    public IWebProxy? Proxy { get; set; }
+}
+
 public abstract class RestCallService {
     private readonly string _baseUrl;
+    public IWebProxy? Proxy { get; set; }
 
     protected RestCallService(string baseUrl) {
         _baseUrl = baseUrl;
     }
 
-    internal async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) {
-        var client = new HttpClient();
+    protected HttpClient CreateClient() {
+        var clientHandler = new HttpClientHandler() {
+            Proxy = Proxy
+        };
+        var client = new HttpClient(handler: clientHandler);
         client.DefaultRequestHeaders.Add("Accept", "application/json");
-        client.DefaultRequestHeaders.Add("Content-Type", "application/json");
+        return client;
+    }
+
+    internal async Task<T?> RestCall<T>(Uri uri, HttpMethod method, string body) {
+        var client = CreateClient();
 
         var request = new HttpRequestMessage();
         request.Method = method;
         request.Content = new StringContent(body, Encoding.UTF8, "application/json");
         request.RequestUri = uri;
-
         var response = await client.SendAsync(request);
 
         if (response.IsSuccessStatusCode)
diff --git a/Services/Interfaces/IOAuthService.cs b/Services/Interfaces/IOAuthService.cs
index 5ea4616e..801fdd5c 100644
--- a/Services/Interfaces/IOAuthService.cs
+++ b/Services/Interfaces/IOAuthService.cs
@@ -1,8 +1,9 @@
+using FitConnect.RestService;
 using FitConnect.Services.Models;
 
 namespace FitConnect.Services;
 
-public interface IOAuthService {
+public interface IOAuthService: IRestCallService {
     /// <summary>
     ///     Requesting an OAuth token from the FitConnect API.
     ///     <para>
diff --git a/Services/OAuthService.cs b/Services/OAuthService.cs
index c9bb472d..00f97803 100644
--- a/Services/OAuthService.cs
+++ b/Services/OAuthService.cs
@@ -2,14 +2,15 @@ using System.Net;
 using System.Net.Http.Headers;
 using System.Net.Http.Json;
 using System.Security.Authentication;
+using FitConnect.RestService;
 using FitConnect.Services.Models;
 
 namespace FitConnect.Services;
 
-public class OAuthService : IOAuthService {
+public class OAuthService : RestCallService, IOAuthService {
     private readonly string _tokenUrl;
 
-    public OAuthService(string tokenUrl) {
+    public OAuthService(string tokenUrl) : base(tokenUrl) {
         _tokenUrl = tokenUrl;
     }
 
@@ -27,9 +28,7 @@ public class OAuthService : IOAuthService {
     /// <returns>The received token or null</returns>
     public async Task<OAuthAccessToken?> AuthenticateAsync(string clientId, string clientSecret,
         string? scope = null) {
-        var client = new HttpClient();
-        client.DefaultRequestHeaders.Accept.Add(
-            MediaTypeWithQualityHeaderValue.Parse("application/json"));
+        var client = CreateClient();
 
         var requestContent = new Dictionary<string, string> {
             { "grant_type", "client_credentials" },
-- 
GitLab