Skip to content

Commit

Permalink
[SG-648] BEEEP-Refactor DuoApi class to use Httpclient (#2691)
Browse files Browse the repository at this point in the history
* Started work on refactoring class

* Added duo api respons model

* Made httpclient version of APICall

* Added more properties to response model

* Refactored duo api class to use httpclient

* Removed unuseful comments

* Fixed lint formatting
  • Loading branch information
gbubemismith authored Feb 24, 2023
1 parent 38336dd commit f11c58e
Show file tree
Hide file tree
Showing 4 changed files with 59 additions and 63 deletions.
5 changes: 2 additions & 3 deletions src/Api/Controllers/TwoFactorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
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;
Expand Down Expand Up @@ -153,7 +152,7 @@ public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDu
try
{
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
duoApi.JSONApiCall<object>("GET", "/auth/v2/check");
await duoApi.JSONApiCall("GET", "/auth/v2/check");
}
catch (DuoException)
{
Expand Down Expand Up @@ -210,7 +209,7 @@ public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
try
{
var duoApi = new DuoApi(model.IntegrationKey, model.SecretKey, model.Host);
duoApi.JSONApiCall<object>("GET", "/auth/v2/check");
await duoApi.JSONApiCall("GET", "/auth/v2/check");
}
catch (DuoException)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Api/Models/Request/TwoFactorRequestModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Utilities;
using Fido2NetLib;

namespace Bit.Api.Models.Request;
Expand Down Expand Up @@ -104,7 +105,7 @@ public Organization ToOrganization(Organization extistingOrg)

public override IEnumerable<ValidationResult> 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) });
}
Expand Down
27 changes: 27 additions & 0 deletions src/Core/Models/Api/Response/Duo/DuoResponseModel.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
87 changes: 28 additions & 59 deletions src/Core/Utilities/DuoApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ All rights reserved
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
{
Expand All @@ -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;
Expand Down Expand Up @@ -92,20 +95,14 @@ public string Sign(string method, string path, string canonParams, string date)
return string.Concat("Basic ", Encode64(auth));
}

public string ApiCall(string method, string path, Dictionary<string, string> parameters = null)
{
return ApiCall(method, path, parameters, 0, out var statusCode);
}

/// <param name="timeout">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
/// calls (particularly in the Auth APIs) will not
/// 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.</param>
public string ApiCall(string method, string path, Dictionary<string, string> parameters, int timeout,
out HttpStatusCode statusCode)
private async Task<(string result, HttpStatusCode statusCode)> ApiCall(string method, string path, Dictionary<string, string> parameters, int timeout)
{
if (parameters == null)
{
Expand All @@ -121,58 +118,39 @@ public string ApiCall(string method, string path, Dictionary<string, string> par
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 (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;
_httpClient.Timeout = TimeSpan.FromMilliseconds(timeout);
}

// 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()))
if (method.Equals("POST") || method.Equals("PUT"))
{
statusCode = response.StatusCode;
return reader.ReadToEnd();
request.Content = new StringContent(canonParams, Encoding.UTF8, "application/x-www-form-urlencoded");
}

var response = await _httpClient.SendAsync(request);
var result = await response.Content.ReadAsStringAsync();
var statusCode = response.StatusCode;
return (result, statusCode);
}

public T JSONApiCall<T>(string method, string path, Dictionary<string, string> parameters = null)
where T : class
public async Task<Response> JSONApiCall(string method, string path, Dictionary<string, string> parameters = null)
{
return JSONApiCall<T>(method, path, parameters, 0);
return await JSONApiCall(method, path, parameters, 0);
}

/// <param name="timeout">The request timeout, in milliseconds.
Expand All @@ -182,27 +160,18 @@ public T JSONApiCall<T>(string method, string path, Dictionary<string, string> p
/// 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.</param>
public T JSONApiCall<T>(string method, string path, Dictionary<string, string> parameters, int timeout)
where T : class
private async Task<Response> JSONApiCall(string method, string path, Dictionary<string, string> 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<Dictionary<string, object>>(res);
if (dict["stat"].ToString() == "OK")
var obj = JsonSerializer.Deserialize<DuoResponseModel>(res);
if (obj.Stat == "OK")
{
return JsonSerializer.Deserialize<T>(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)
{
Expand Down

0 comments on commit f11c58e

Please sign in to comment.