diff --git a/src/LetsEncrypt/Services/LetsEncryptService.cs b/src/LetsEncrypt/Services/LetsEncryptService.cs index daab003..e11f7d3 100644 --- a/src/LetsEncrypt/Services/LetsEncryptService.cs +++ b/src/LetsEncrypt/Services/LetsEncryptService.cs @@ -13,7 +13,6 @@ using MaksIT.LetsEncrypt.Models.Interfaces; using MaksIT.LetsEncrypt.Models.Requests; using MaksIT.LetsEncrypt.Models.Responses; using MaksIT.Results; -using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using System.Net.Http.Headers; @@ -68,33 +67,51 @@ public class LetsEncryptService : ILetsEncryptService { // Helper: Send ACME request and process response private async Task> SendAcmeRequest(HttpRequestMessage request, State state, HttpMethod method) { var response = await _httpClient.SendAsync(request); + UpdateStateNonceIfNeeded(response, state, method); + var responseText = await response.Content.ReadAsStringAsync(); + HandleProblemResponseAsync(response, responseText); + return ProcessResponseContent(response, responseText); } // Helper: Poll challenge status until valid or timeout private async Task PollChallengeStatus(Guid sessionId, AuthorizationChallengeChallenge challenge, State state) { - if (challenge?.Url == null) return Result.InternalServerError("Challenge URL is null"); + if (challenge?.Url == null) + return Result.InternalServerError("Challenge URL is null"); + var start = DateTime.UtcNow; + while (true) { var pollRequest = new HttpRequestMessage(HttpMethod.Post, challenge.Url); + await HandleNonceAsync(sessionId, challenge.Url, state); + var pollJson = EncodeMessage(true, null, state, new JwsHeader { Url = challenge.Url, Nonce = state.Nonce }); + PrepareRequestContent(pollRequest, pollJson, HttpMethod.Post); + var pollResponse = await _httpClient.SendAsync(pollRequest); + UpdateStateNonceIfNeeded(pollResponse, state, HttpMethod.Post); + var pollResponseText = await pollResponse.Content.ReadAsStringAsync(); + HandleProblemResponseAsync(pollResponse, pollResponseText); + var authChallenge = ProcessResponseContent(pollResponse, pollResponseText); + if (authChallenge.Result?.Status != "pending") return authChallenge.Result?.Status == "valid" ? Result.Ok() : Result.InternalServerError(); + if ((DateTime.UtcNow - start).Seconds > 120) return Result.InternalServerError("Timeout"); + await Task.Delay(1000); } } @@ -103,16 +120,30 @@ public class LetsEncryptService : ILetsEncryptService { public async Task ConfigureClient(Guid sessionId, bool isStaging) { try { var state = GetOrCreateState(sessionId); + state.IsStaging = isStaging; + _httpClient.BaseAddress ??= new Uri(isStaging ? _appSettings.Staging : _appSettings.Production); + if (state.Directory == null) { var request = new HttpRequestMessage(HttpMethod.Get, new Uri(DirectoryEndpoint, UriKind.Relative)); + await HandleNonceAsync(sessionId, new Uri(DirectoryEndpoint, UriKind.Relative), state); + var directory = await SendAcmeRequest(request, state, HttpMethod.Get); + state.Directory = directory.Result ?? throw new InvalidOperationException("Directory response is null"); } - return Result.Ok(); - } catch (Exception ex) { + + return Result.Ok("Client configured successfully."); + } + catch (LetsEncrytException ex) { + List messages = ["Let's Encrypt client encountered a problem"]; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError([.. messages]); + } + catch (Exception ex) { const string message = "Let's Encrypt client unhandled exception"; _logger.LogError(ex, message); return Result.InternalServerError(message); @@ -123,45 +154,68 @@ public class LetsEncryptService : ILetsEncryptService { #region Init public async Task Init(Guid sessionId, Guid accountId, string description, string[] contacts, RegistrationCache? cache) { if (sessionId == Guid.Empty) { - _logger.LogError("Invalid sessionId"); - return Result.InternalServerError(); + const string message = "Invalid sessionId"; + _logger.LogError(message); + return Result.InternalServerError(message); } + if (contacts == null || contacts.Length == 0) { - _logger.LogError("Contacts are null or empty"); - return Result.InternalServerError(); + const string message = "Contacts are null or empty"; + _logger.LogError(message); + return Result.InternalServerError(message); } + var state = GetOrCreateState(sessionId); + if (state.Directory == null) { - _logger.LogError("State directory is null"); - return Result.InternalServerError(); + const string message = "State directory is null"; + _logger.LogError(message); + return Result.InternalServerError(message); } + _logger.LogInformation($"Executing {nameof(Init)}..."); + try { var accountKey = new RSACryptoServiceProvider(4096); + if (cache != null && cache.AccountKey != null) { state.Cache = cache; + accountKey.ImportCspBlob(cache.AccountKey); + state.JwsService = new JwsService(accountKey); + state.JwsService.SetKeyId(cache.Location?.ToString() ?? string.Empty); - } else { + } + else { state.JwsService = new JwsService(accountKey); + var letsEncryptOrder = new Account { TermsOfServiceAgreed = true, - Contacts = contacts.Select(contact => $"mailto:{contact}").ToArray() + Contacts = [.. contacts.Select(contact => $"mailto:{contact}")] }; + var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewAccount); + await HandleNonceAsync(sessionId, state.Directory.NewAccount, state); + var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader { Url = state.Directory.NewAccount, Nonce = state.Nonce }); + PrepareRequestContent(request, json, HttpMethod.Post); + var result = await SendAcmeRequest(request, state, HttpMethod.Post); + state.JwsService.SetKeyId(result.Result?.Location?.ToString() ?? string.Empty); + if (result.Result?.Status != "valid") { - _logger.LogError($"Account status is not valid, was: {result.Result?.Status} \r\n {result.ResponseText}"); - return Result.InternalServerError(); + var errorMessage = $"Account status is not valid, was: {result.Result?.Status} \r\n {result.ResponseText}"; + _logger.LogError(errorMessage); + return Result.InternalServerError(errorMessage); } + state.Cache = new RegistrationCache { AccountId = accountId, Description = description, @@ -173,8 +227,16 @@ public class LetsEncryptService : ILetsEncryptService { Key = result.Result.Key }; } - return Result.Ok(); - } catch (Exception ex) { + + return Result.Ok("Initialization successful."); + } + catch (LetsEncrytException ex) { + List messages = ["Let's Encrypt client encountered a problem"]; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError([.. messages]); + } + catch (Exception ex) { const string message = "Let's Encrypt client unhandled exception"; _logger.LogError(ex, message); return Result.InternalServerError(message); @@ -184,8 +246,10 @@ public class LetsEncryptService : ILetsEncryptService { public Result GetRegistrationCache(Guid sessionId) { var state = GetOrCreateState(sessionId); + if(state?.Cache == null) return Result.InternalServerError(null); + return Result.Ok(state.Cache); } @@ -193,15 +257,20 @@ public class LetsEncryptService : ILetsEncryptService { public Result GetTermsOfServiceUri(Guid sessionId) { try { var state = GetOrCreateState(sessionId); + _logger.LogInformation($"Executing {nameof(GetTermsOfServiceUri)}..."); + if (state.Directory?.Meta?.TermsOfService == null) { return Result.Ok(null); } + return Result.Ok(state.Directory.Meta.TermsOfService); - } catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - _logger.LogError(ex, message); - return Result.InternalServerError(message); + } + catch (Exception ex) { + List messages = new List { "Let's Encrypt client unhandled exception" }; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError(null, [.. messages]); } } #endregion @@ -210,61 +279,96 @@ public class LetsEncryptService : ILetsEncryptService { public async Task?>> NewOrder(Guid sessionId, string[] hostnames, string challengeType) { try { var state = GetOrCreateState(sessionId); + _logger.LogInformation($"Executing {nameof(NewOrder)}..."); + state.Challenges.Clear(); + var letsEncryptOrder = new Order { Expires = DateTime.UtcNow.AddDays(2), Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier { Type = DnsType, Value = hostname ?? string.Empty - }).ToArray() ?? Array.Empty() + }).ToArray() ?? [] }; + if (state.Directory == null || state.Directory.NewOrder == null) return Result?>.InternalServerError(null); + var request = new HttpRequestMessage(HttpMethod.Post, state.Directory.NewOrder); + await HandleNonceAsync(sessionId, state.Directory.NewOrder, state); + var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader { Url = state.Directory.NewOrder, Nonce = state.Nonce }); + PrepareRequestContent(request, json, HttpMethod.Post); + var order = await SendAcmeRequest(request, state, HttpMethod.Post); + if (StatusEquals(order.Result?.Status, OrderStatus.Ready)) return Result?>.Ok(new Dictionary()); + if (!StatusEquals(order.Result?.Status, OrderStatus.Pending)) { _logger.LogError($"Created new order and expected status '{OrderStatus.Pending.GetDisplayName()}', but got: {order.Result?.Status} \r\n {order.Result}"); return Result?>.InternalServerError(null); } + state.CurrentOrder = order.Result; + var results = new Dictionary(); + foreach (var item in state.CurrentOrder?.Authorizations ?? Array.Empty()) { - if (item == null) continue; + if (item == null) + continue; + request = new HttpRequestMessage(HttpMethod.Post, item); + await HandleNonceAsync(sessionId, item, state); + json = EncodeMessage(true, null, state, new JwsHeader { Url = item, Nonce = state.Nonce }); + PrepareRequestContent(request, json, HttpMethod.Post); + var challengeResponse = await SendAcmeRequest(request, state, HttpMethod.Post); + if (StatusEquals(challengeResponse.Result?.Status, OrderStatus.Valid)) continue; + if (!StatusEquals(challengeResponse.Result?.Status, OrderStatus.Pending)) { _logger.LogError($"Expected authorization status '{OrderStatus.Pending.GetDisplayName()}', but got: {state.CurrentOrder?.Status} \r\n {challengeResponse.ResponseText}"); return Result?>.InternalServerError(null); } - var challenge = challengeResponse.Result?.Challenges?.FirstOrDefault(x => x?.Type == challengeType); + + var challenge = challengeResponse.Result?.Challenges? + .FirstOrDefault(x => x?.Type == challengeType); + if (challenge == null || challenge.Token == null) { _logger.LogError("Challenge or token is null"); return Result?>.InternalServerError(null); } + state.Challenges.Add(challenge); - if (state.Cache != null) state.Cache.ChallengeType = challengeType; - var keyToken = state.JwsService != null ? state.JwsService.GetKeyAuthorization(challenge.Token) : string.Empty; + + if (state.Cache != null) + state.Cache.ChallengeType = challengeType; + + var keyToken = state.JwsService != null + ? state.JwsService.GetKeyAuthorization(challenge.Token) + : string.Empty; + switch (challengeType) { case "dns-01": using (var sha256 = SHA256.Create()) { - var dnsToken = state.JwsService != null ? state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken ?? string.Empty))) : string.Empty; + var dnsToken = state.JwsService != null + ? state.JwsService.Base64UrlEncoded(sha256.ComputeHash(Encoding.UTF8.GetBytes(keyToken ?? string.Empty))) + : string.Empty; + results[challengeResponse.Result?.Identifier?.Value ?? string.Empty] = dnsToken; } break; @@ -277,10 +381,12 @@ public class LetsEncryptService : ILetsEncryptService { } return Result?>.Ok(results); - } catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - _logger.LogError(ex, message); - return Result?>.InternalServerError(null, message); + } + catch (Exception ex) { + List messages = new List { "Let's Encrypt client unhandled exception" }; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result?>.InternalServerError(null, [.. messages]); } } #endregion @@ -289,33 +395,46 @@ public class LetsEncryptService : ILetsEncryptService { public async Task CompleteChallenges(Guid sessionId) { try { var state = GetOrCreateState(sessionId); + _logger.LogInformation($"Executing {nameof(CompleteChallenges)}..."); + if (state.CurrentOrder?.Identifiers == null) { return Result.InternalServerError("Current order identifiers are null"); } + for (var index = 0; index < state.Challenges.Count; index++) { var challenge = state.Challenges[index]; + if (challenge?.Url == null) { _logger.LogError("Challenge URL is null"); return Result.InternalServerError(); } + var request = new HttpRequestMessage(HttpMethod.Post, challenge.Url); + await HandleNonceAsync(sessionId, challenge.Url, state); + var json = EncodeMessage(false, "{}", state, new JwsHeader { Url = challenge.Url, Nonce = state.Nonce }); + PrepareRequestContent(request, json, HttpMethod.Post); + var authChallenge = await SendAcmeRequest(request, state, HttpMethod.Post); + var result = await PollChallengeStatus(sessionId, challenge, state); + if (!result.IsSuccess) return result; } return Result.Ok(); - } catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - _logger.LogError(ex, message); - return Result.InternalServerError(message); + } + catch (Exception ex) { + List messages = new List { "Let's Encrypt client unhandled exception" }; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError([.. messages]); } } #endregion @@ -324,28 +443,39 @@ public class LetsEncryptService : ILetsEncryptService { public async Task GetOrder(Guid sessionId, string[] hostnames) { try { _logger.LogInformation($"Executing {nameof(GetOrder)}"); + var state = GetOrCreateState(sessionId); + var letsEncryptOrder = new Order { Expires = DateTime.UtcNow.AddDays(2), Identifiers = hostnames?.Where(h => h != null).Select(hostname => new OrderIdentifier { Type = "dns", Value = hostname! - }).ToArray() ?? Array.Empty() + }).ToArray() ?? [] }; + var request = new HttpRequestMessage(HttpMethod.Post, state.Directory!.NewOrder); + await HandleNonceAsync(sessionId, state.Directory.NewOrder, state); + var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader { Url = state.Directory.NewOrder, Nonce = state.Nonce }); + PrepareRequestContent(request, json, HttpMethod.Post); + var order = await SendAcmeRequest(request, state, HttpMethod.Post); + state.CurrentOrder = order.Result; + return Result.Ok(); - } catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - _logger.LogError(ex, message); - return Result.InternalServerError(message); + } + catch (Exception ex) { + List messages = new List { "Let's Encrypt client unhandled exception" }; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError([.. messages]); } } #endregion @@ -354,85 +484,122 @@ public class LetsEncryptService : ILetsEncryptService { public async Task GetCertificate(Guid sessionId, string subject) { try { var state = GetOrCreateState(sessionId); + _logger.LogInformation($"Executing {nameof(GetCertificate)}..."); + if (state.CurrentOrder?.Identifiers == null) { return Result.InternalServerError(); } + var key = new RSACryptoServiceProvider(4096); - var csr = new CertificateRequest("CN=" + subject, - key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var csr = new CertificateRequest("CN=" + subject, key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var san = new SubjectAlternativeNameBuilder(); + foreach (var host in state.CurrentOrder.Identifiers) { if (host?.Value != null) san.AddDnsName(host.Value); } + csr.CertificateExtensions.Add(san.Build()); + var letsEncryptOrder = new FinalizeRequest { Csr = state.JwsService!.Base64UrlEncoded(csr.CreateSigningRequest()) }; + Uri? certificateUrl = default; + var start = DateTime.UtcNow; + while (certificateUrl == null) { - var hostnames = state.CurrentOrder.Identifiers?.Select(x => x?.Value).Where(x => x != null).Cast().ToArray() ?? Array.Empty(); + var hostnames = state.CurrentOrder?.Identifiers?.Select(x => x?.Value).Where(x => x != null).Cast().ToArray() ?? []; + await GetOrder(sessionId, hostnames); + var status = state.CurrentOrder?.Status; + if (StatusEquals(status, OrderStatus.Ready)) { - var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize!); + var request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Finalize); + await HandleNonceAsync(sessionId, state.CurrentOrder.Finalize!, state); + var json = EncodeMessage(false, letsEncryptOrder, state, new JwsHeader { Url = state.CurrentOrder.Finalize, Nonce = state.Nonce }); + PrepareRequestContent(request, json, HttpMethod.Post); + var order = await SendAcmeRequest(request, state, HttpMethod.Post); + if (StatusEquals(order.Result?.Status, OrderStatus.Processing)) { request = new HttpRequestMessage(HttpMethod.Post, state.CurrentOrder.Location!); + await HandleNonceAsync(sessionId, state.CurrentOrder.Location!, state); + json = EncodeMessage(true, null, state, new JwsHeader { Url = state.CurrentOrder.Location, Nonce = state.Nonce }); + PrepareRequestContent(request, json, HttpMethod.Post); + order = await SendAcmeRequest(request, state, HttpMethod.Post); } + if (StatusEquals(order.Result?.Status, OrderStatus.Valid)) { certificateUrl = order.Result.Certificate; } - } else if (StatusEquals(status, OrderStatus.Valid)) { + } + else if (StatusEquals(status, OrderStatus.Valid)) { certificateUrl = state.CurrentOrder.Certificate; break; } + if ((DateTime.UtcNow - start).Seconds > 120) throw new TimeoutException(); + await Task.Delay(1000); } + var finalRequest = new HttpRequestMessage(HttpMethod.Post, certificateUrl!); + await HandleNonceAsync(sessionId, certificateUrl!, state); + var finalJson = EncodeMessage(true, null, state, new JwsHeader { Url = certificateUrl, Nonce = state.Nonce }); + PrepareRequestContent(finalRequest, finalJson, HttpMethod.Post); + var pem = await SendAcmeRequest(finalRequest, state, HttpMethod.Post); + if (state.Cache == null) { _logger.LogError($"{nameof(state.Cache)} is null"); return Result.InternalServerError(); } + state.Cache.CachedCerts ??= new Dictionary(); + state.Cache.CachedCerts[subject] = new CertificateCache { Cert = pem.Result ?? string.Empty, Private = key.ExportCspBlob(true), PrivatePem = key.ExportRSAPrivateKeyPem() }; + var certPem = pem.Result ?? string.Empty; + if (!string.IsNullOrEmpty(certPem)) { var cert = new X509Certificate2(Encoding.UTF8.GetBytes(certPem)); } + return Result.Ok(); - } catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - _logger.LogError(ex, message); - return Result.InternalServerError(message); + } + catch (Exception ex) { + List messages = new List { "Let's Encrypt client unhandled exception" }; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError([.. messages]); } } #endregion @@ -444,58 +611,86 @@ public class LetsEncryptService : ILetsEncryptService { public async Task RevokeCertificate(Guid sessionId, string subject, RevokeReason reason) { try { var state = GetOrCreateState(sessionId); + _logger.LogInformation($"Executing {nameof(RevokeCertificate)}..."); + if (state.Cache?.CachedCerts == null || !state.Cache.CachedCerts.TryGetValue(subject, out var certificateCache) || certificateCache == null) { _logger.LogError("Certificate not found in cache"); return Result.InternalServerError("Certificate not found"); } + var certPem = certificateCache.Cert ?? string.Empty; + if (string.IsNullOrEmpty(certPem)) { _logger.LogError("Certificate PEM is null or empty"); return Result.InternalServerError("Certificate PEM is null or empty"); } + var certificate = new X509Certificate2(Encoding.UTF8.GetBytes(certPem)); + var derEncodedCert = certificate.Export(X509ContentType.Cert); + var base64UrlEncodedCert = state.JwsService!.Base64UrlEncoded(derEncodedCert); + var revokeRequest = new RevokeRequest { Certificate = base64UrlEncodedCert, Reason = (int)reason }; + var request = new HttpRequestMessage(HttpMethod.Post, state.Directory!.RevokeCert); + await HandleNonceAsync(sessionId, state.Directory.RevokeCert, state); + var jwsHeader = new JwsHeader { Url = state.Directory.RevokeCert, Nonce = state.Nonce }; + var json = state.JwsService.Encode(revokeRequest, jwsHeader).ToJson(); + request.Content = new StringContent(json); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(GetContentType(ContentType.JoseJson)); + var response = await _httpClient.SendAsync(request); + UpdateStateNonceIfNeeded(response, state, HttpMethod.Post); + var responseText = await response.Content.ReadAsStringAsync(); + if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) { var erroObj = responseText.ToObject(); } + if (!response.IsSuccessStatusCode) Result.InternalServerError(responseText); + state.Cache.CachedCerts.Remove(subject); _logger.LogInformation("Certificate revoked successfully"); + return Result.Ok(); - } catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - _logger.LogError(ex, message); - return Result.InternalServerError($"{message}: {ex.Message}"); + + } + catch (Exception ex) { + List messages = new List { "Let's Encrypt client unhandled exception" }; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError([.. messages]); } } #region SendAsync private async Task HandleNonceAsync(Guid sessionId, Uri uri, State state) { - if (uri == null) throw new ArgumentNullException(nameof(uri)); + if (uri == null) + throw new ArgumentNullException(nameof(uri)); + if (uri.OriginalString != "directory") { var newNonceResult = await NewNonce(sessionId); + if (!newNonceResult.IsSuccess || newNonceResult.Value == null) { throw new InvalidOperationException("Failed to retrieve nonce."); } + state.Nonce = newNonceResult.Value; } else { @@ -506,17 +701,23 @@ public class LetsEncryptService : ILetsEncryptService { private async Task> NewNonce(Guid sessionId) { try { var state = GetOrCreateState(sessionId); + _logger.LogInformation($"Executing {nameof(NewNonce)}..."); + if (state.Directory?.NewNonce == null) return Result.InternalServerError(null); + var result = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, state.Directory.NewNonce)); + var nonce = result.Headers.GetValues("Replay-Nonce").FirstOrDefault(); + return Result.Ok(nonce); } catch (Exception ex) { - var message = "Let's Encrypt client unhandled exception"; - _logger.LogError(ex, message); - return Result.InternalServerError(null, message); + List messages = new List { "Let's Encrypt client unhandled exception" }; + _logger.LogError(ex, messages.FirstOrDefault()); + messages.Add(ex.Message); + return Result.InternalServerError(null, [.. messages]); } } @@ -530,13 +731,17 @@ public class LetsEncryptService : ILetsEncryptService { private void PrepareRequestContent(HttpRequestMessage request, string json, HttpMethod method) { request.Content = new StringContent(json ?? string.Empty); - var contentType = method == HttpMethod.Post ? GetContentType(ContentType.JoseJson) : GetContentType(ContentType.Json); + var contentType = method == HttpMethod.Post + ? GetContentType(ContentType.JoseJson) + : GetContentType(ContentType.Json); request.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); } private void HandleProblemResponseAsync(HttpResponseMessage response, string responseText) { if (response.Content.Headers.ContentType?.MediaType == GetContentType(ContentType.ProblemJson)) { - throw new LetsEncrytException(responseText.ToObject(), response); + var problem = responseText.ToObject(); + + throw new LetsEncrytException(problem, response); } }