diff --git a/src/Api/Controllers/TwoFactorController.cs b/src/Api/Controllers/TwoFactorController.cs index 07d7197d66..244d3c7387 100644 --- a/src/Api/Controllers/TwoFactorController.cs +++ b/src/Api/Controllers/TwoFactorController.cs @@ -11,7 +11,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; -using Bit.Core.Utilities.Duo; using Fido2NetLib; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -153,7 +152,7 @@ public class TwoFactorController : Controller try { var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - duoApi.JSONApiCall("GET", "/auth/v2/check"); + await duoApi.JSONApiCall("GET", "/auth/v2/check"); } catch (DuoException) { @@ -210,7 +209,7 @@ public class TwoFactorController : Controller try { var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host); - duoApi.JSONApiCall("GET", "/auth/v2/check"); + await duoApi.JSONApiCall("GET", "/auth/v2/check"); } catch (DuoException) { diff --git a/src/Api/Models/Request/TwoFactorRequestModels.cs b/src/Api/Models/Request/TwoFactorRequestModels.cs index 18ba944ea8..a2b236edfa 100644 --- a/src/Api/Models/Request/TwoFactorRequestModels.cs +++ b/src/Api/Models/Request/TwoFactorRequestModels.cs @@ -3,6 +3,7 @@ using Bit.Api.Models.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models; +using Bit.Core.Utilities; using Fido2NetLib; namespace Bit.Api.Models.Request; @@ -104,7 +105,7 @@ public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IV public override IEnumerable Validate(ValidationContext validationContext) { - if (!Core.Utilities.Duo.DuoApi.ValidHost(Host)) + if (!DuoApi.ValidHost(Host)) { yield return new ValidationResult("Host is invalid.", new string[] { nameof(Host) }); } diff --git a/src/Core/Models/Api/Response/Duo/DuoResponseModel.cs b/src/Core/Models/Api/Response/Duo/DuoResponseModel.cs new file mode 100644 index 0000000000..573d77ab0c --- /dev/null +++ b/src/Core/Models/Api/Response/Duo/DuoResponseModel.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; + +namespace Bit.Core.Models.Api.Response.Duo; + +public class DuoResponseModel +{ + [JsonPropertyName("stat")] + public string Stat { get; set; } + + [JsonPropertyName("code")] + public int? Code { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + + [JsonPropertyName("message_detail")] + public string MessageDetail { get; set; } + + [JsonPropertyName("response")] + public Response Response { get; set; } +} + +public class Response +{ + [JsonPropertyName("time")] + public int Time { get; set; } +} diff --git a/src/Core/Utilities/DuoApi.cs b/src/Core/Utilities/DuoApi.cs index b5a3f040d4..662ca643d7 100644 --- a/src/Core/Utilities/DuoApi.cs +++ b/src/Core/Utilities/DuoApi.cs @@ -15,8 +15,9 @@ using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using System.Web; +using Bit.Core.Models.Api.Response.Duo; -namespace Bit.Core.Utilities.Duo; +namespace Bit.Core.Utilities; public class DuoApi { @@ -27,6 +28,8 @@ public class DuoApi private readonly string _ikey; private readonly string _skey; + private readonly HttpClient _httpClient = new(); + public DuoApi(string ikey, string skey, string host) { _ikey = ikey; @@ -92,11 +95,6 @@ public class DuoApi return string.Concat("Basic ", Encode64(auth)); } - public string ApiCall(string method, string path, Dictionary parameters = null) - { - return ApiCall(method, path, parameters, 0, out var statusCode); - } - /// The request timeout, in milliseconds. /// Specify 0 to use the system-default timeout. Use caution if /// you choose to specify a custom timeout - some API @@ -104,8 +102,7 @@ public class DuoApi /// return a response until an out-of-band authentication process /// has completed. In some cases, this may take as much as a /// small number of minutes. - public string ApiCall(string method, string path, Dictionary parameters, int timeout, - out HttpStatusCode statusCode) + private async Task<(string result, HttpStatusCode statusCode)> ApiCall(string method, string path, Dictionary parameters, int timeout) { if (parameters == null) { @@ -121,58 +118,39 @@ public class DuoApi query = "?" + canonParams; } } - var url = string.Format("{0}://{1}{2}{3}", UrlScheme, _host, path, query); + var url = $"{UrlScheme}://{_host}{path}{query}"; var dateString = RFC822UtcNow(); var auth = Sign(method, path, canonParams, dateString); - var request = (HttpWebRequest)WebRequest.Create(url); - request.Method = method; - request.Accept = "application/json"; + var request = new HttpRequestMessage + { + Method = new HttpMethod(method), + RequestUri = new Uri(url), + }; request.Headers.Add("Authorization", auth); request.Headers.Add("X-Duo-Date", dateString); - request.UserAgent = UserAgent; + request.Headers.UserAgent.ParseAdd(UserAgent); + + if (timeout > 0) + { + _httpClient.Timeout = TimeSpan.FromMilliseconds(timeout); + } if (method.Equals("POST") || method.Equals("PUT")) { - var data = Encoding.UTF8.GetBytes(canonParams); - request.ContentType = "application/x-www-form-urlencoded"; - request.ContentLength = data.Length; - using (var requestStream = request.GetRequestStream()) - { - requestStream.Write(data, 0, data.Length); - } - } - if (timeout > 0) - { - request.Timeout = timeout; + request.Content = new StringContent(canonParams, Encoding.UTF8, "application/x-www-form-urlencoded"); } - // Do the request and process the result. - HttpWebResponse response; - try - { - response = (HttpWebResponse)request.GetResponse(); - } - catch (WebException ex) - { - response = (HttpWebResponse)ex.Response; - if (response == null) - { - throw; - } - } - using (var reader = new StreamReader(response.GetResponseStream())) - { - statusCode = response.StatusCode; - return reader.ReadToEnd(); - } + var response = await _httpClient.SendAsync(request); + var result = await response.Content.ReadAsStringAsync(); + var statusCode = response.StatusCode; + return (result, statusCode); } - public T JSONApiCall(string method, string path, Dictionary parameters = null) - where T : class + public async Task JSONApiCall(string method, string path, Dictionary parameters = null) { - return JSONApiCall(method, path, parameters, 0); + return await JSONApiCall(method, path, parameters, 0); } /// The request timeout, in milliseconds. @@ -182,27 +160,18 @@ public class DuoApi /// return a response until an out-of-band authentication process /// has completed. In some cases, this may take as much as a /// small number of minutes. - public T JSONApiCall(string method, string path, Dictionary parameters, int timeout) - where T : class + private async Task JSONApiCall(string method, string path, Dictionary parameters, int timeout) { - var res = ApiCall(method, path, parameters, timeout, out var statusCode); + var (res, statusCode) = await ApiCall(method, path, parameters, timeout); try { - // TODO: We should deserialize this into our own DTO and not work on dictionaries. - var dict = JsonSerializer.Deserialize>(res); - if (dict["stat"].ToString() == "OK") + var obj = JsonSerializer.Deserialize(res); + if (obj.Stat == "OK") { - return JsonSerializer.Deserialize(dict["response"].ToString()); + return obj.Response; } - var check = ToNullableInt(dict["code"].ToString()); - var code = check.GetValueOrDefault(0); - var messageDetail = string.Empty; - if (dict.ContainsKey("message_detail")) - { - messageDetail = dict["message_detail"].ToString(); - } - throw new ApiException(code, (int)statusCode, dict["message"].ToString(), messageDetail); + throw new ApiException(obj.Code ?? 0, (int)statusCode, obj.Message, obj.MessageDetail); } catch (ApiException) {