diff --git a/.github/workflows/net-ci.yml b/.github/workflows/net-ci.yml
index 4027702..78b822e 100644
--- a/.github/workflows/net-ci.yml
+++ b/.github/workflows/net-ci.yml
@@ -4,10 +4,10 @@ on:
push:
branches:
- master
+ - main
+ - feature/*
pull_request:
- branches:
- - master
-
+
jobs:
# Build and test on .NET Core
dotnet-core-ci:
@@ -18,9 +18,9 @@ jobs:
- uses: actions/checkout@v2
- name: Set up .NET
- uses: actions/setup-dotnet@v1.7.2
+ uses: actions/setup-dotnet@v3
with:
- dotnet-version: ${{ matrix.dotnet-version }}
+ dotnet-version: 6.0.x
- name: Install dependencies
run: nuget restore
@@ -55,4 +55,4 @@ jobs:
run: msbuild.exe duo_api_csharp.sln
- name: Run Tests dll
- run: vstest.console.exe .\test\bin\Debug\DuoApiTest.dll
+ run: vstest.console.exe .\DuoApi.Tests\bin\Debug\net6.0\DuoApi.Tests.dll
diff --git a/DuoApi.Examples/DuoApi.Examples.csproj b/DuoApi.Examples/DuoApi.Examples.csproj
new file mode 100644
index 0000000..58e1ee6
--- /dev/null
+++ b/DuoApi.Examples/DuoApi.Examples.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Exe
+ net6.0
+ True
+ True
+
+
+
+
+
+
+
diff --git a/examples/Program.cs b/DuoApi.Examples/Program.cs
similarity index 76%
rename from examples/Program.cs
rename to DuoApi.Examples/Program.cs
index 110606b..ef6c6ce 100644
--- a/examples/Program.cs
+++ b/DuoApi.Examples/Program.cs
@@ -23,8 +23,7 @@ static int Main(string[] args)
var r = client.JSONApiCall>(
"GET", "/admin/v1/info/authentication_attempts", parameters);
var attempts = r["authentication_attempts"] as Dictionary;
- foreach (KeyValuePair info in attempts)
- {
+ foreach (KeyValuePair info in attempts) {
var s = String.Format("{0} authentication(s) ended with {1}.",
info.Value,
info.Key);
@@ -35,28 +34,23 @@ static int Main(string[] args)
var users = client.JSONApiCall(
"GET", "/admin/v1/users", parameters);
System.Console.WriteLine(String.Format("{0} users.", users.Count));
- foreach (Dictionary user in users)
- {
+ foreach (Dictionary user in users) {
System.Console.WriteLine(
"\t" + "Username: " + (user["username"] as string));
}
// paging call
int? offset = 0;
- while (offset != null)
- {
- var jsonResponse = client.JSONPagingApiCall("GET", "/admin/v1/users", parameters, (int)offset, 10);
- var pagedUsers = jsonResponse["response"] as System.Collections.ArrayList;
+ while (offset != null) {
+ var pagedUsers = client.JSONPagingApiCall("GET", "/admin/v1/users", parameters, (int)offset, 10, out var metadata);
System.Console.WriteLine(String.Format("{0} users at offset {1}", pagedUsers.Count, offset));
- foreach (Dictionary user in pagedUsers)
- {
+ foreach (Dictionary user in pagedUsers) {
System.Console.WriteLine(
"\t" + "Username: " + (user["username"] as string));
}
- var metadata = jsonResponse["metadata"] as Dictionary;
- if (metadata.ContainsKey("next_offset"))
+ if (metadata.next_offset.HasValue)
{
- offset = metadata["next_offset"] as int?;
+ offset = metadata.next_offset.Value;
}
else
{
diff --git a/test/ApiCallTest.cs b/DuoApi.Tests/ApiCallTest.cs
similarity index 95%
rename from test/ApiCallTest.cs
rename to DuoApi.Tests/ApiCallTest.cs
index e2d27fc..7abaa82 100644
--- a/test/ApiCallTest.cs
+++ b/DuoApi.Tests/ApiCallTest.cs
@@ -391,10 +391,10 @@ public void TestValidJsonPagingResponseNoParameters()
return "{\"stat\": \"OK\", \"response\": \"hello, world!\", \"metadata\": {\"next_offset\":10}}";
};
var parameters = new Dictionary();
- var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 0, 10);
- Assert.Equal("hello, world!", jsonResponse["response"]);
- var metadata = jsonResponse["metadata"] as Dictionary;
- Assert.Equal(10, metadata["next_offset"]);
+ var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 0, 10, out var metadata);
+ Assert.Equal("hello, world!", jsonResponse);
+
+ Assert.Equal(10, metadata.next_offset.Value);
// make sure parameters was not changed as a side-effect
Assert.Empty(parameters);
}
@@ -411,10 +411,10 @@ public void TestValidJsonPagingResponseExistingParameters()
{"offset", "0"},
{"limit", "10"}
};
- var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 10, 20);
- Assert.Equal("hello, world!", jsonResponse["response"]);
- var metadata = jsonResponse["metadata"] as Dictionary;
- Assert.False(metadata.ContainsKey("next_offset"));
+ var jsonResponse = api.JSONPagingApiCall("GET", "/json_ok", parameters, 10, 20, out var metadata);
+ Assert.Equal("hello, world!", jsonResponse);
+
+ Assert.NotNull(metadata);
// make sure parameters was not changed as a side-effect
Assert.Equal(2, parameters.Count);
Assert.Equal("0", parameters["offset"]);
@@ -461,7 +461,7 @@ public void TestJsonResponseMissingField()
});
Assert.NotNull(ex);
- var e = Assert.IsType(ex);
+ var e = Assert.IsType(ex);
Assert.Equal(400, e.HttpStatus);
@@ -527,7 +527,7 @@ public void TestRateLimitedCompletely()
HttpStatusCode code;
string response = api.ApiCall("GET", "/hello", new Dictionary(), 10000, out code);
- Assert.Equal(code, (HttpStatusCode)429);
+ Assert.Equal((HttpStatusCode)429, code);
Assert.Equal(7, callCount);
Assert.Equal(6, api.sleeper.sleepCalls.Count);
Assert.Equal(1123, api.sleeper.sleepCalls[0]);
diff --git a/test/CertPinningTest.cs b/DuoApi.Tests/CertPinningTest.cs
similarity index 100%
rename from test/CertPinningTest.cs
rename to DuoApi.Tests/CertPinningTest.cs
diff --git a/DuoApi.Tests/DuoApi.Tests.csproj b/DuoApi.Tests/DuoApi.Tests.csproj
new file mode 100644
index 0000000..3b56383
--- /dev/null
+++ b/DuoApi.Tests/DuoApi.Tests.csproj
@@ -0,0 +1,31 @@
+
+
+
+ net6.0
+
+ false
+
+ True
+
+ True
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/test/QueryParamsTest.cs b/DuoApi.Tests/QueryParamsTest.cs
similarity index 99%
rename from test/QueryParamsTest.cs
rename to DuoApi.Tests/QueryParamsTest.cs
index 70956bb..76613bf 100644
--- a/test/QueryParamsTest.cs
+++ b/DuoApi.Tests/QueryParamsTest.cs
@@ -106,4 +106,4 @@ public void NextOffsetTest()
var expected = "foo=1&next_offset=fjaewoifjew&next_offset=473891274832917498";
Assert.Equal(expected, DuoApi.CanonicalizeParams(parameters));
}
-}
+}
\ No newline at end of file
diff --git a/test/SigningTest.cs b/DuoApi.Tests/SigningTest.cs
similarity index 99%
rename from test/SigningTest.cs
rename to DuoApi.Tests/SigningTest.cs
index 4c5d933..c078157 100644
--- a/test/SigningTest.cs
+++ b/DuoApi.Tests/SigningTest.cs
@@ -31,4 +31,4 @@ public void HmacSha512()
var expected = "Basic dGVzdF9pa2V5OjA1MDgwNjUwMzVhMDNiMmExZGUyZjQ1M2U2MjllNzkxZDE4MDMyOWUxNTdmNjVkZjZiM2UwZjA4Mjk5ZDQzMjFlMWM1YzdhN2M3ZWU2YjllNWZjODBkMWZiNmZiZjNhZDVlYjdjNDRkZDNiMzk4NWEwMmMzN2FjYTUzZWMzNjk4";
Assert.Equal(expected, actual);
}
-}
+}
\ No newline at end of file
diff --git a/DuoApi.Tests/TestRealAPICall.cs b/DuoApi.Tests/TestRealAPICall.cs
new file mode 100644
index 0000000..5b0fd19
--- /dev/null
+++ b/DuoApi.Tests/TestRealAPICall.cs
@@ -0,0 +1,103 @@
+using Duo;
+using System;
+using System.Linq;
+using System.Text.Json;
+using Xunit;
+
+public class TestRealAPICall
+{
+ ///
+ /// Secrets should not be committed, a true integration test cannot be be performed without these.
+ ///
+ private const string test_ikey = "INTEGRATION KEY";
+ private const string test_skey = "SECRET KEY";
+ private const string test_host = "api-.duosecurity.com";
+
+ private DuoApi api;
+
+ ///
+ ///
+ ///
+ public TestRealAPICall()
+ {
+ api = new DuoApi(test_ikey, test_skey, test_host);
+ }
+
+ [SkippableTheory]
+ [InlineData(1)]
+ [InlineData(1, 1)]
+ [InlineData(400)]
+ public void GetUsers(ushort pagesize, ushort offset = 0)
+ {
+ Skip.If(test_ikey == "INTEGRATION KEY", "The keys are not configure");
+ Skip.If(test_skey == "SECRET KEY", "The keys are not configure");
+
+ //arrange
+ //act
+ var users = api.GetUsers(pagesize, out var pagingInfo, offset);
+
+ //assert
+ Console.WriteLine($"{users.Length:n0} users");
+ Console.WriteLine(JsonSerializer.Serialize(users, new JsonSerializerOptions() { WriteIndented = true }));
+
+ Assert.True(users.Length <= pagesize);
+ }
+
+ [SkippableFact]
+ public void GetAllTest()
+ {
+ Skip.If(test_ikey == "INTEGRATION KEY", "The keys are not configure");
+ Skip.If(test_skey == "SECRET KEY", "The keys are not configure");
+
+ //arrange
+ ushort pagesize = 2;
+ ushort offset = 0;
+
+ //act
+ var users = api.GetUsers(pagesize, out var pagingInfo, offset).ToList();
+
+ while (pagingInfo.next_offset.HasValue)
+ {
+ offset = pagingInfo.next_offset.Value;
+
+ users.AddRange(api.GetUsers(pagesize, out pagingInfo, offset));
+ }
+
+ //assert
+ Console.WriteLine($"{users.Count:n0} users");
+ Console.WriteLine(JsonSerializer.Serialize(users, new JsonSerializerOptions()
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault
+ }));
+
+ Assert.Equal(users.Count, pagingInfo.total_objects);
+ }
+
+ [SkippableFact]
+ public void GetSingleTest()
+ {
+ Skip.If(test_ikey == "INTEGRATION KEY", "The keys are not configure");
+ Skip.If(test_skey == "SECRET KEY", "The keys are not configure");
+
+ //arrange
+ var user = api.GetUsers(100, out _).LastOrDefault();
+
+ Skip.If(user is null);
+
+
+ //act
+ var actual = api.GetUser(user.UserName);
+
+ //assert
+ Assert.NotNull(actual);
+ Assert.Equal(actual.User_Id, user.User_Id);
+
+ Console.WriteLine(JsonSerializer.Serialize(actual, new JsonSerializerOptions()
+ {
+ WriteIndented = true,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingDefault
+ }));
+ }
+
+}
diff --git a/duo_api_csharp/CertificatePinnerFactory.cs b/DuoApi/CertificatePinnerFactory.cs
similarity index 96%
rename from duo_api_csharp/CertificatePinnerFactory.cs
rename to DuoApi/CertificatePinnerFactory.cs
index 8026eb1..d1cdcbb 100644
--- a/duo_api_csharp/CertificatePinnerFactory.cs
+++ b/DuoApi/CertificatePinnerFactory.cs
@@ -1,4 +1,4 @@
-/*
+/*
* Copyright (c) 2022 Cisco Systems, Inc. and/or its affiliates
* All rights reserved
*/
@@ -13,10 +13,16 @@
namespace Duo
{
+ ///
+ ///
+ ///
public class CertificatePinnerFactory
{
private readonly X509CertificateCollection _rootCerts;
+ ///
+ /// Certificate Pinner Factory
+ ///
public CertificatePinnerFactory(X509CertificateCollection rootCerts)
{
_rootCerts = rootCerts;
@@ -130,7 +136,7 @@ internal static X509CertificateCollection GetDuoCertCollection()
internal static string[] ReadCertsFromFile()
{
var certs = "";
- using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("duo_api_csharp.ca_certs.pem"))
+ using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("DuoApi.ca_certs.pem"))
using (StreamReader reader = new StreamReader(stream))
{
certs = reader.ReadToEnd();
diff --git a/DuoApi/DataEnvelope.cs b/DuoApi/DataEnvelope.cs
new file mode 100644
index 0000000..f5b7daa
--- /dev/null
+++ b/DuoApi/DataEnvelope.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Duo
+{
+
+ ///
+ ///
+ ///
+ ///
+ internal class DataEnvelope
+ {
+ ///
+ ///
+ ///
+ [Required]
+ public DuoApiResponseStatus Stat { get; set; }
+
+ ///
+ ///
+ ///
+ public int? Code { get; set; }
+
+ ///
+ ///
+ ///
+#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+ public T Response { get; set; }
+#pragma warning restore CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider declaring as nullable.
+
+ ///
+ /// Upon error, basic error information
+ ///
+ public string? Message { get; set; }
+
+ ///
+ /// Upon error, detailed error information
+ ///
+ public string? Message_detail { get; set; }
+
+ ///
+ ///
+ ///
+ public PagingInfo? Metadata { get; set; }
+ }
+}
diff --git a/duo_api_csharp/Duo.cs b/DuoApi/Duo.cs
similarity index 68%
rename from duo_api_csharp/Duo.cs
rename to DuoApi/Duo.cs
index 5b8ea18..60e944c 100644
--- a/duo_api_csharp/Duo.cs
+++ b/DuoApi/Duo.cs
@@ -4,26 +4,28 @@
*/
using System;
-using System.Configuration;
using System.Collections.Generic;
+using System.Configuration;
+using System.Globalization;
using System.IO;
using System.Net;
+using System.Net.Security;
+using System.Runtime.InteropServices;
using System.Security.Cryptography;
-using System.Text.RegularExpressions;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
-using System.Web.Script.Serialization;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
using System.Web;
-using System.Globalization;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Security.Cryptography.X509Certificates;
-using System.Net.Security;
-
namespace Duo
{
- public class DuoApi
+ public partial class DuoApi
{
+ ///
+ /// The default user agent string sent to DUO
+ ///
public string DEFAULT_AGENT = "DuoAPICSharp/1.0";
private const int INITIAL_BACKOFF_MS = 1000;
@@ -38,7 +40,7 @@ public class DuoApi
private SleepService sleepService;
private RandomService randomService;
private bool sslCertValidation = true;
- private X509CertificateCollection customRoots = null;
+ private X509CertificateCollection? customRoots = null;
// TLS 1.0/1.1 deprecation effective June 30, 2023
// Of the SecurityProtocolType enum, it should be noted that SystemDefault is not available prior to .NET 4.7 and TLS 1.3 is not available prior to .NET 4.8.
@@ -66,12 +68,23 @@ public DuoApi(string ikey, string skey, string host)
/// Duo secret key
/// Application secret key
/// HTTP client User-Agent
- public DuoApi(string ikey, string skey, string host, string user_agent)
+ public DuoApi(string ikey, string skey, string host, string? user_agent)
: this(ikey, skey, host, user_agent, "https", new ThreadSleepService(), new SystemRandomService())
{
}
- protected DuoApi(string ikey, string skey, string host, string user_agent, string url_scheme,
+ ///
+ /// Protected constructor, allowing injection of and .
+ /// Also allows specifying non-https .
+ ///
+ /// Duo integration key
+ /// Duo secret key
+ /// Application secret key
+ /// HTTP client User-Agent
+ ///
+ ///
+ ///
+ protected DuoApi(string ikey, string skey, string host, string? user_agent, string url_scheme,
SleepService sleepService, RandomService randomService)
{
this.ikey = ikey;
@@ -80,14 +93,10 @@ protected DuoApi(string ikey, string skey, string host, string user_agent, strin
this.url_scheme = url_scheme;
this.sleepService = sleepService;
this.randomService = randomService;
- if (String.IsNullOrEmpty(user_agent))
- {
- this.user_agent = FormatUserAgent(DEFAULT_AGENT);
- }
- else
- {
- this.user_agent = user_agent;
- }
+
+ this.user_agent = string.IsNullOrEmpty(user_agent)
+ ? FormatUserAgent(DEFAULT_AGENT)
+ : user_agent;
}
///
@@ -116,7 +125,7 @@ public DuoApi UseCustomRootCertificates(X509CertificateCollection customRoots)
return this;
}
- public static string FinishCanonicalize(string p)
+ internal static string FinishCanonicalize(string p)
{
// Signatures require upper-case hex digits.
p = Regex.Replace(p,
@@ -134,12 +143,12 @@ public static string FinishCanonicalize(string p)
return p;
}
- public static string CanonicalizeParams(Dictionary parameters)
+ internal static string CanonicalizeParams(Dictionary parameters)
{
- var ret = new List();
+ var ret = new List();
foreach (KeyValuePair pair in parameters)
{
- string p = String.Format("{0}={1}",
+ string p = string.Format("{0}={1}",
HttpUtility.UrlEncode(pair.Key),
HttpUtility.UrlEncode(pair.Value));
@@ -152,9 +161,9 @@ public static string CanonicalizeParams(Dictionary parameters)
// handle value as an object eg. next_offset = ["123", "fdajkld"]
- public static string CanonicalizeParams(Dictionary parameters)
+ internal static string CanonicalizeParams(Dictionary parameters)
{
- var ret = new List();
+ var ret = new List();
foreach (KeyValuePair pair in parameters)
{
string p = "";
@@ -163,7 +172,7 @@ public static string CanonicalizeParams(Dictionary parameters)
string[] values = (string[])pair.Value;
string value1 = values[0];
string value2 = values[1];
- p = String.Format("{0}={1}&{2}={3}",
+ p = string.Format("{0}={1}&{2}={3}",
HttpUtility.UrlEncode(pair.Key),
HttpUtility.UrlEncode(value1),
HttpUtility.UrlEncode(pair.Key),
@@ -172,7 +181,7 @@ public static string CanonicalizeParams(Dictionary parameters)
else
{
string val = (string)pair.Value;
- p = String.Format("{0}={1}",
+ p = string.Format("{0}={1}",
HttpUtility.UrlEncode(pair.Key),
HttpUtility.UrlEncode(val));
}
@@ -184,7 +193,7 @@ public static string CanonicalizeParams(Dictionary parameters)
}
- protected string CanonicalizeRequest(string method,
+ internal string CanonicalizeRequest(string method,
string path,
string canon_params,
string date)
@@ -196,12 +205,13 @@ protected string CanonicalizeRequest(string method,
path,
canon_params,
};
- string canon = String.Join("\n",
+ string canon = string.Join("\n",
lines);
return canon;
}
- public string Sign(string method,
+
+ internal string Sign(string method,
string path,
string canon_params,
string date)
@@ -215,7 +225,7 @@ public string Sign(string method,
return "Basic " + DuoApi.Encode64(auth);
}
- public string ApiCall(string method,
+ internal string ApiCall(string method,
string path,
Dictionary parameters)
{
@@ -223,6 +233,9 @@ public string ApiCall(string method,
return ApiCall(method, path, parameters, 0, DateTime.UtcNow, out 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
@@ -230,19 +243,24 @@ public string ApiCall(string method,
/// 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,
+ ///
+ internal string ApiCall(string method,
string path,
Dictionary parameters,
int timeout,
out HttpStatusCode statusCode)
{
- return ApiCall(method, path, parameters, 0, DateTime.UtcNow, out statusCode);
+ return ApiCall(method, path, parameters, timeout, DateTime.UtcNow, out statusCode);
}
+ ///
+ ///
+ ///
/// The current date and time, used to authenticate
/// the API request. Typically, you should specify DateTime.UtcNow,
/// but if you do not wish to rely on the system-wide clock, you may
/// determine the current date/time by some other means.
+ ///
/// 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
@@ -285,8 +303,8 @@ StreamReader reader
return reader.ReadToEnd();
}
- private HttpWebRequest PrepareHttpRequest(String method, String url, String auth, String date,
- String cannonParams, int timeout)
+ private HttpWebRequest PrepareHttpRequest(string method, string url, string auth, string date,
+ string cannonParams, int timeout)
{
ServicePointManager.SecurityProtocol = SelectSecurityProtocolType;
@@ -297,9 +315,12 @@ private HttpWebRequest PrepareHttpRequest(String method, String url, String auth
request.Headers.Add("Authorization", auth);
request.Headers.Add("X-Duo-Date", date);
request.UserAgent = this.user_agent;
+
+ //todo: Understand, handle and test proxy config
+
// If no proxy, check for and use WinHTTP proxy as autoconfig won't pick this up when run from a service
- if (!HasProxyServer(request))
- request.Proxy = GetWinhttpProxy();
+ //if (!HasProxyServer(request))
+ //request.Proxy = GetWinhttpProxy();
if (method.Equals("POST") || method.Equals("PUT"))
{
@@ -336,7 +357,7 @@ private RemoteCertificateValidationCallback GetCertificatePinner()
}
private HttpWebResponse AttemptRetriableHttpRequest(
- String method, String url, String auth, String date, String cannonParams, int timeout)
+ string method, string url, string auth, string date, string cannonParams, int timeout)
{
int backoffMs = INITIAL_BACKOFF_MS;
while (true)
@@ -367,55 +388,60 @@ private HttpWebResponse AttemptRetriableHttpRequest(
}
}
+ ///
+
+ ///
+ ///
/// The current date and time, used to authenticate
/// the API request. Typically, you should specify DateTime.UtcNow,
/// but if you do not wish to rely on the system-wide clock, you may
/// determine the current date/time by some other means.
+ ///
/// 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 complete JSON response.
/// raises if JSON response indicates an error
- private Dictionary BaseJSONApiCall(string method,
+ private T BaseJSONApiCall(string method,
string path,
Dictionary parameters,
int timeout,
- DateTime date)
+ DateTime date,
+ out PagingInfo? metaData)
{
HttpStatusCode statusCode;
string res = this.ApiCall(method, path, parameters, timeout, date, out statusCode);
- var jss = new JavaScriptSerializer();
try
{
- var dict = jss.Deserialize>(res);
- if (dict["stat"] as string == "OK")
+ var options = new JsonSerializerOptions
{
- return dict;
+ PropertyNameCaseInsensitive = true
+ };
+
+ options.Converters.Add(new JsonStringEnumConverter());
+
+ var dict = JsonSerializer.Deserialize>(res, options);
+
+ if(dict is null)
+ throw new BadResponseException(0,res);
+
+ if (dict.Stat == DuoApiResponseStatus.Ok)
+ {
+ metaData = dict.Metadata;
+ return dict.Response;
}
else
{
- int? check = dict["code"] as int?;
- int code;
- if (check.HasValue)
- {
- code = check.Value;
- }
- else
- {
- code = 0;
- }
- String message_detail = "";
- if (dict.ContainsKey("message_detail"))
- {
- message_detail = dict["message_detail"] as string;
- }
+
+ int code = dict.Code.GetValueOrDefault();
+
throw new ApiException(code,
(int)statusCode,
- dict["message"] as string,
- message_detail);
+ dict.Message,
+ dict.Message_detail);
}
}
catch (ApiException)
@@ -424,10 +450,11 @@ private Dictionary BaseJSONApiCall(string method,
}
catch (Exception e)
{
- throw new BadResponseException((int)statusCode, e);
+ throw new BadResponseException((int)statusCode,res, e);
}
}
+ ///
public T JSONApiCall(string method,
string path,
Dictionary parameters)
@@ -436,6 +463,9 @@ public T JSONApiCall(string method,
return JSONApiCall(method, path, parameters, 0, DateTime.UtcNow);
}
+ ///
+ ///
+ ///
/// 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
@@ -452,6 +482,10 @@ public T JSONApiCall(string method,
return JSONApiCall(method, path, parameters, timeout, DateTime.UtcNow);
}
+ ///
+
+ ///
+ ///
/// The current date and time, used to authenticate
/// the API request. Typically, you should specify DateTime.UtcNow,
/// but if you do not wish to rely on the system-wide clock, you may
@@ -470,23 +504,30 @@ public T JSONApiCall(string method,
DateTime date)
where T : class
{
- var dict = BaseJSONApiCall(method, path, parameters, timeout, date);
- return dict["response"] as T;
+ return BaseJSONApiCall(method, path, parameters, timeout, date, out _);
}
- public Dictionary JSONPagingApiCall(string method,
+
+ ///
+ public T JSONPagingApiCall(string method,
string path,
Dictionary parameters,
int offset,
- int limit)
+ int limit,
+ out PagingInfo? metaData)
{
- return JSONPagingApiCall(method, path, parameters, offset, limit, 0, DateTime.UtcNow);
+ return JSONPagingApiCall(method, path, parameters, offset, limit, 0, DateTime.UtcNow, out metaData);
}
+ ///
+
+ ///
+ ///
/// The current date and time, used to authenticate
/// the API request. Typically, you should specify DateTime.UtcNow,
/// but if you do not wish to rely on the system-wide clock, you may
/// determine the current date/time by some other means.
+ ///
/// 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
@@ -503,20 +544,21 @@ public Dictionary JSONPagingApiCall(string method,
/// return a JSON dictionary with top level keys: stat, response, metadata.
/// The actual requested data is in 'response'. 'metadata' contains a
/// 'next_offset' key which should be used to fetch the next page.
- public Dictionary JSONPagingApiCall(string method,
+ public T JSONPagingApiCall(string method,
string path,
Dictionary parameters,
int offset,
int limit,
int timeout,
- DateTime date)
+ DateTime date,
+ out PagingInfo? metaData)
{
// copy parameters so we don't cause any side-effects
parameters = new Dictionary(parameters);
parameters["offset"] = offset.ToString(); // overrides caller value
parameters["limit"] = limit.ToString();
- return this.BaseJSONApiCall(method, path, parameters, timeout, date);
+ return this.BaseJSONApiCall(method, path, parameters, timeout, date, out metaData);
}
@@ -525,7 +567,7 @@ public Dictionary JSONPagingApiCall(string method,
/// e.g. "FooClient/1.0"
public static string FormatUserAgent(string product_name)
{
- return String.Format(
+ return string.Format(
"{0} ({1}; .NET {2})", product_name, System.Environment.OSVersion,
System.Environment.Version);
}
@@ -578,98 +620,98 @@ private static string DateToRFC822(DateTime date)
return date_string;
}
- ///
- /// Gets the WinHTTP proxy.
- ///
- ///
- /// Normally, C# picks up these proxy settings by default, but when run under the SYSTEM account, it does not.
- ///
- ///
- private static System.Net.WebProxy GetWinhttpProxy()
- {
- string[] proxyServerNames = null;
- string primaryProxyServer = null;
- string[] bypassHostnames = null;
- bool enableLocalBypass = false;
- System.Net.WebProxy winhttpProxy = null;
-
- // Has a proxy been configured?
- // No. Is a WinHTTP proxy set?
- int internetHandle = WinHttpOpen("DuoTest", WinHttp_Access_Type.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, null, null, 0);
- if (internetHandle != 0)
- {
- // Yes, use it. This is normal when run under the SYSTEM account and a WinHTTP proxy is configured. When run as a normal user,
- // the Proxy property will already be configured correctly. To resolve this for SYSTEM, manually read proxy settings and configure.
- var proxyInfo = new WINHTTP_PROXY_INFO();
- WinHttpGetDefaultProxyConfiguration(proxyInfo);
- if (proxyInfo.lpszProxy != null)
- {
- if (proxyInfo.lpszProxy != null)
- {
- proxyServerNames = proxyInfo.lpszProxy.Split(new char[] { ' ', '\t', ';' });
- if ((proxyServerNames == null) || (proxyServerNames.Length == 0))
- primaryProxyServer = proxyInfo.lpszProxy;
- else
- primaryProxyServer = proxyServerNames[0];
- }
- if (proxyInfo.lpszProxyBypass != null)
- {
- bypassHostnames = proxyInfo.lpszProxyBypass.Split(new char[] { ' ', '\t', ';' });
- if ((bypassHostnames == null) || (bypassHostnames.Length == 0))
- bypassHostnames = new string[] { proxyInfo.lpszProxyBypass };
- if (bypassHostnames != null)
- enableLocalBypass = bypassHostnames.Contains("local", StringComparer.InvariantCultureIgnoreCase);
- }
- if (primaryProxyServer != null)
- winhttpProxy = new System.Net.WebProxy(proxyServerNames[0], enableLocalBypass, bypassHostnames);
- }
- WinHttpCloseHandle(internetHandle);
- internetHandle = 0;
- }
- else
- {
- throw new Exception(String.Format("WinHttp init failed {0}", System.Runtime.InteropServices.Marshal.GetLastWin32Error()));
- }
-
- return winhttpProxy;
- }
-
- ///
- /// Determines if the specified web request is using a proxy server.
- ///
- ///
- /// If no proxy is set, the Proxy member is typically non-null and set to an object type that includes but hides IWebProxy with no address,
- /// so it cannot be inspected. Resolving this requires reflection to extract the hidden webProxy object and check it's Address member.
- ///
- /// Request to check
- /// TRUE if a proxy is in use, else FALSE
- public static bool HasProxyServer(HttpWebRequest requestObject)
- {
- WebProxy actualProxy = null;
- bool hasProxyServer = false;
-
- if (requestObject.Proxy != null)
- {
- // WebProxy is described as the base class for IWebProxy, so we should always see this type as the field is initialized by the framework.
- if (!(requestObject.Proxy is WebProxy))
- {
- var webProxyField = requestObject.Proxy.GetType().GetField("webProxy", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
- if (webProxyField != null)
- actualProxy = webProxyField.GetValue(requestObject.Proxy) as WebProxy;
- }
- else
- {
- actualProxy = requestObject.Proxy as WebProxy;
- }
- hasProxyServer = (actualProxy.Address != null);
- }
- else
- {
- hasProxyServer = false;
- }
-
- return hasProxyServer;
- }
+ /////
+ ///// Gets the WinHTTP proxy.
+ /////
+ /////
+ ///// Normally, C# picks up these proxy settings by default, but when run under the SYSTEM account, it does not.
+ /////
+ /////
+ //private static System.Net.WebProxy GetWinhttpProxy()
+ //{
+ // string[] proxyServerNames = null;
+ // string primaryProxyServer = null;
+ // string[] bypassHostnames = null;
+ // bool enableLocalBypass = false;
+ // System.Net.WebProxy winhttpProxy = null;
+
+ // // Has a proxy been configured?
+ // // No. Is a WinHTTP proxy set?
+ // int internetHandle = WinHttpOpen("DuoTest", WinHttp_Access_Type.WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, null, null, 0);
+ // if (internetHandle != 0)
+ // {
+ // // Yes, use it. This is normal when run under the SYSTEM account and a WinHTTP proxy is configured. When run as a normal user,
+ // // the Proxy property will already be configured correctly. To resolve this for SYSTEM, manually read proxy settings and configure.
+ // var proxyInfo = new WINHTTP_PROXY_INFO();
+ // WinHttpGetDefaultProxyConfiguration(proxyInfo);
+ // if (proxyInfo.lpszProxy != null)
+ // {
+ // if (proxyInfo.lpszProxy != null)
+ // {
+ // proxyServerNames = proxyInfo.lpszProxy.Split(new char[] { ' ', '\t', ';' });
+ // if ((proxyServerNames == null) || (proxyServerNames.Length == 0))
+ // primaryProxyServer = proxyInfo.lpszProxy;
+ // else
+ // primaryProxyServer = proxyServerNames[0];
+ // }
+ // if (proxyInfo.lpszProxyBypass != null)
+ // {
+ // bypassHostnames = proxyInfo.lpszProxyBypass.Split(new char[] { ' ', '\t', ';' });
+ // if ((bypassHostnames == null) || (bypassHostnames.Length == 0))
+ // bypassHostnames = new string[] { proxyInfo.lpszProxyBypass };
+ // if (bypassHostnames != null)
+ // enableLocalBypass = bypassHostnames.Contains("local", StringComparer.InvariantCultureIgnoreCase);
+ // }
+ // if (primaryProxyServer != null)
+ // winhttpProxy = new System.Net.WebProxy(proxyServerNames[0], enableLocalBypass, bypassHostnames);
+ // }
+ // WinHttpCloseHandle(internetHandle);
+ // internetHandle = 0;
+ // }
+ // else
+ // {
+ // throw new Exception(String.Format("WinHttp init failed {0}", System.Runtime.InteropServices.Marshal.GetLastWin32Error()));
+ // }
+
+ // return winhttpProxy;
+ //}
+
+ /////
+ ///// Determines if the specified web request is using a proxy server.
+ /////
+ /////
+ ///// If no proxy is set, the Proxy member is typically non-null and set to an object type that includes but hides IWebProxy with no address,
+ ///// so it cannot be inspected. Resolving this requires reflection to extract the hidden webProxy object and check it's Address member.
+ /////
+ ///// Request to check
+ ///// TRUE if a proxy is in use, else FALSE
+ //public static bool HasProxyServer(HttpWebRequest requestObject)
+ //{
+ // WebProxy actualProxy = null;
+ // bool hasProxyServer = false;
+
+ // if (requestObject.Proxy != null)
+ // {
+ // // WebProxy is described as the base class for IWebProxy, so we should always see this type as the field is initialized by the framework.
+ // if (!(requestObject.Proxy is WebProxy))
+ // {
+ // var webProxyField = requestObject.Proxy.GetType().GetField("webProxy", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Public);
+ // if (webProxyField != null)
+ // actualProxy = webProxyField.GetValue(requestObject.Proxy) as WebProxy;
+ // }
+ // else
+ // {
+ // actualProxy = requestObject.Proxy as WebProxy;
+ // }
+ // hasProxyServer = (actualProxy.Address != null);
+ // }
+ // else
+ // {
+ // hasProxyServer = false;
+ // }
+
+ // return hasProxyServer;
+ //}
#endregion Private Methods
#region Private DllImport
@@ -689,9 +731,9 @@ private class WINHTTP_PROXY_INFO
{
public int dwAccessType;
[MarshalAs(UnmanagedType.LPWStr)]
- public string lpszProxy;
+ public string? lpszProxy;
[MarshalAs(UnmanagedType.LPWStr)]
- public string lpszProxyBypass;
+ public string? lpszProxyBypass;
}
[DllImport("winhttp.dll", CharSet = CharSet.Unicode)]
private static extern bool WinHttpGetDefaultProxyConfiguration([In, Out] WINHTTP_PROXY_INFO proxyInfo);
@@ -707,35 +749,49 @@ private static extern int WinHttpOpen([MarshalAs(UnmanagedType.LPWStr)] string p
private static extern bool WinHttpCloseHandle(int hInternet);
#endregion Private DllImport
}
-
+
+
+ ///
[Serializable]
public class DuoException : Exception
{
+
+ ///
public int HttpStatus { get; private set; }
- public DuoException(int http_status, string message, Exception inner)
+
+ ///
+ public DuoException(int http_status, string message, Exception? inner)
: base(message, inner)
{
this.HttpStatus = http_status;
}
+ ///
protected DuoException(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext ctxt)
: base(info, ctxt)
{ }
}
+ ///
[Serializable]
public class ApiException : DuoException
{
+ ///
public int Code { get; private set; }
- public string ApiMessage { get; private set; }
- public string ApiMessageDetail { get; private set; }
+ ///
+ public string? ApiMessage { get; private set; }
+
+ ///
+ public string? ApiMessageDetail { get; private set; }
+
+ ///
public ApiException(int code,
int http_status,
- string api_message,
- string api_message_detail)
+ string? api_message,
+ string? api_message_detail)
: base(http_status, FormatMessage(code, api_message, api_message_detail), null)
{
this.Code = code;
@@ -743,51 +799,72 @@ public ApiException(int code,
this.ApiMessageDetail = api_message_detail;
}
+
+ ///
protected ApiException(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext ctxt)
: base(info, ctxt)
{ }
private static string FormatMessage(int code,
- string api_message,
- string api_message_detail)
+ string? api_message,
+ string? api_message_detail)
{
- return String.Format(
+ return string.Format(
"Duo API Error {0}: '{1}' ('{2}')", code, api_message, api_message_detail);
}
}
+ ///
[Serializable]
public class BadResponseException : DuoException
{
- public BadResponseException(int http_status, Exception inner)
- : base(http_status, FormatMessage(http_status, inner), inner)
- { }
+ ///
+ public string? Response { get; private set; }
+
+ ///
+ public BadResponseException(int http_status, string? response = null, Exception? inner = null)
+ : base(http_status, FormatMessage(http_status, response, inner), inner)
+ {
+ Response = response;
+ }
+ ///
protected BadResponseException(System.Runtime.Serialization.SerializationInfo info,
System.Runtime.Serialization.StreamingContext ctxt)
: base(info, ctxt)
- { }
+ {
+ Response = info.GetString("Response");
+ }
- private static string FormatMessage(int http_status, Exception inner)
+ private static string FormatMessage(int http_status, string? response, Exception? inner)
{
string inner_message = "(null)";
if (inner != null)
{
- inner_message = String.Format("'{0}'", inner.Message);
+ inner_message = string.Format("'{0}'", inner.Message);
}
- return String.Format(
- "Got error {0} with HTTP Status {1}", inner_message, http_status);
+ return $"Got error {inner_message} with HTTP Status {http_status}. Response : {response}";
}
}
-
+ ///
+ /// Service that implements waiting (unknown why this is injectable)
+ ///
public interface SleepService
{
+
+ ///
void Sleep(int ms);
}
+
+ ///
+ /// Service that implements getting a random number (unknown why this is injectable)
+ ///
public interface RandomService
{
+
+ ///
int GetInt(int maxInt);
}
diff --git a/DuoApi/DuoApi.csproj b/DuoApi/DuoApi.csproj
new file mode 100644
index 0000000..506cd3f
--- /dev/null
+++ b/DuoApi/DuoApi.csproj
@@ -0,0 +1,46 @@
+
+
+
+ netstandard2.1
+ enable
+ True
+
+
+ 0.0.2.3
+ A client for DUO Admin API
+
+
+ True
+ True
+ True
+ True
+
+ duo admin
+ https://duo.com/docs/adminapi
+ https://github.com/duosecurity/duo_api_csharp.git
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/DuoApi/DuoApiMethods.cs b/DuoApi/DuoApiMethods.cs
new file mode 100644
index 0000000..c30d11c
--- /dev/null
+++ b/DuoApi/DuoApiMethods.cs
@@ -0,0 +1,50 @@
+using Duo.Models;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.Linq;
+
+namespace Duo
+{
+ ///
+ /// A DUO Admin Api client
+ ///
+ public partial class DuoApi
+ {
+ ///
+ /// Get paged users
+ ///
+ ///
+ ///
+ ///
+ ///
+ public User[] GetUsers(
+ [Range(1,300)]
+ ushort limit,
+ out PagingInfo? pagingInfo,
+ ushort offset = 0)
+ {
+ var parameters = new Dictionary();
+ var users = this.JSONPagingApiCall(
+ "GET", "/admin/v1/users", parameters,offset, limit, out pagingInfo);
+
+ return users;
+ }
+
+ ///
+ /// Gets a single user by .
+ ///
+ /// The username of the user.
+ ///
+ /// Username appears to be recyclable, but should be unique at a given point in time.
+ /// is permanently uniquely identifying a .
+ ///
+ public User GetUser(string userName)
+ {
+ var parameters = new Dictionary { { "username" , userName } };
+ var users = this.JSONApiCall(
+ "GET", "/admin/v1/users", parameters);
+
+ return users.SingleOrDefault();
+ }
+ }
+}
diff --git a/DuoApi/DuoApiResponseStatus.cs b/DuoApi/DuoApiResponseStatus.cs
new file mode 100644
index 0000000..6f5bad4
--- /dev/null
+++ b/DuoApi/DuoApiResponseStatus.cs
@@ -0,0 +1,7 @@
+namespace Duo
+{
+ internal enum DuoApiResponseStatus {
+ Fail = 0,
+ Ok = 1
+ }
+}
diff --git a/DuoApi/Models/User.cs b/DuoApi/Models/User.cs
new file mode 100644
index 0000000..8144d49
--- /dev/null
+++ b/DuoApi/Models/User.cs
@@ -0,0 +1,142 @@
+using System;
+using System.Text.Json.Serialization;
+
+namespace Duo.Models
+{
+
+ ///
+ ///
+ ///
+ ///
+ /// For specs see: https://duo.com/docs/adminapi#retrieve-users
+ public class User {
+
+ ///
+ /// The user's creation date timestamp.
+ ///
+ public int created { private get; set; }
+
+ ///
+ ///
+ ///
+ public DateTime DateCreated
+ {
+ get
+ {
+ return new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(created);
+ }
+ }
+
+ ///
+ /// The user's email address.
+ ///
+ public string? Email { get; set; }
+
+ ///
+ /// The user's given name.
+ ///
+ public string? FirstName { get; set; }
+
+ ///
+ /// List of groups to which this user belongs.See Retrieve Groups for response info.
+ ///
+ /// todo: formalize group structure
+ public object[]? Groups { get; set; }
+
+ ///
+ /// Is true if the user has a phone, hardware token, U2F token, or security key available for authentication.Otherwise, false.
+ ///
+ public bool Is_Enrolled { get; set; }
+
+ ///
+ /// An integer indicating the last update to the user via directory sync as a Unix timestamp, or null if the user has never synced with an external directory or if the directory that originally created the user has been deleted from Duo.
+ ///
+ public int? last_directory_sync { private get; set; }
+
+ ///
+ ///
+ ///
+ public DateTime? LastDirectoryDync { get {
+ return last_directory_sync.HasValue
+ ? new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(last_directory_sync.Value)
+ : (DateTime?)null;
+ }
+ }
+
+ ///
+ /// An integer indicating the last time this user logged in, as a Unix timestamp, or null if the user has not logged in.
+ ///
+ public int? Last_Login { private get; set; }
+
+ ///
+ ///
+ ///
+ public DateTime? LastLogin
+ {
+ get
+ {
+ return Last_Login.HasValue
+ ? new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(Last_Login.Value)
+ : (DateTime?)null;
+ }
+ }
+
+ ///
+ /// The user's surname.
+ ///
+ public string? LastName { get; set; }
+
+ ///
+ /// Notes about this user.Viewable in the Duo Admin Panel.
+ ///
+ public string? Notes { get; set; }
+
+ ///
+ /// A list of phones that this user can use. See Retrieve Phones for descriptions of the phone response values.
+ ///
+ /// todo: formalize phone structure
+ public object[]? Phones { get; set; }
+
+ ///
+ /// The user's real name (or full name).
+ ///
+ public string? RealName { get; set; }
+
+ ///
+ /// The user's status. One of:
+ /// "active" The user must complete secondary authentication.
+ /// "bypass" The user will bypass secondary authentication after completing primary authentication.
+ /// "disabled" The user will not be able to log in.
+ /// "locked out" The user has been automatically locked out due to excessive authentication attempts.
+ /// "pending deletion" The user was marked for deletion by a Duo admin from the Admin Panel, by the system for inactivity, or by directory sync. If not restored within seven days the user is permanently deleted.
+ /// Note that when a user is a member of a group, the group status may override the individual user's status. Group status is not shown in the user response.
+ ///
+ public string? Status { get; set; }
+
+ ///
+ /// A list of tokens that this user can use.See Retrieve Hardware Tokens for descriptions of the response values.
+ ///
+ public object[]? Tokens { get; set; }
+
+ ///
+ /// A list of U2F tokens that this user can use.See Retrieve U2F Tokens for descriptions of the response values.
+ ///
+ public string[]? U2F_Tokens { get; set; }
+
+ ///
+ /// The user's ID.
+ ///
+ public string? User_Id { get; set; }
+
+ ///
+ /// The user's username.
+ ///
+ public string? UserName { get; set; }
+
+ ///
+ /// A list of WebAuthn authenticators that this user can use.See Retrieve WebAuthn Credentials by User ID for descriptions of the response values.
+ ///
+ /// todo: formalize authenticator structure
+ public object[]? WebAuthnCredentials { get; set; }
+ }
+}
diff --git a/DuoApi/PagingInfo.cs b/DuoApi/PagingInfo.cs
new file mode 100644
index 0000000..bc1553b
--- /dev/null
+++ b/DuoApi/PagingInfo.cs
@@ -0,0 +1,17 @@
+namespace Duo
+{
+ ///
+ /// Information of dataset paging
+ ///
+ public class PagingInfo
+ {
+ ///
+ public int total_objects { get; set; }
+
+ ///
+ public ushort? next_offset { get; set; }
+
+ ///
+ public int prev_offset { get; set; }
+ }
+}
diff --git a/DuoApi/TestFriends.cs b/DuoApi/TestFriends.cs
new file mode 100644
index 0000000..35962b1
--- /dev/null
+++ b/DuoApi/TestFriends.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("DuoApi.Tests")]
diff --git a/DuoApi/Testfriend.cs b/DuoApi/Testfriend.cs
new file mode 100644
index 0000000..44fc75f
--- /dev/null
+++ b/DuoApi/Testfriend.cs
@@ -0,0 +1,3 @@
+using System.Runtime.CompilerServices;
+
+[assembly:InternalsVisibleTo("DuoApi.Tests")]
diff --git a/duo_api_csharp/ca_certs.pem b/DuoApi/ca_certs.pem
similarity index 100%
rename from duo_api_csharp/ca_certs.pem
rename to DuoApi/ca_certs.pem
diff --git a/duo_api_csharp.sln b/duo_api_csharp.sln
index 743ca5f..596cdaf 100644
--- a/duo_api_csharp.sln
+++ b/duo_api_csharp.sln
@@ -2,17 +2,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.32126.317
MinimumVisualStudioVersion = 10.0.40219.1
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "duo_api_csharp", "duo_api_csharp\duo_api_csharp.csproj", "{6E96C9D9-0825-4D26-83C7-8A62180F8FB9}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DuoApiTest", "test\DuoApiTest.csproj", "{6B97B9FB-E553-494C-BD50-4BF7DB5C2184}"
-EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E739E3FE-D923-480A-9B01-3B2A623067E3}"
ProjectSection(SolutionItems) = preProject
LICENSE = LICENSE
README.md = README.md
EndProjectSection
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples", "Examples\Examples.csproj", "{C089A10B-646D-407E-A2B8-848C6C522B13}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DuoApi", "DuoApi\DuoApi.csproj", "{83919561-1430-4928-B231-02A77D8B9AD3}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DuoApi.Tests", "DuoApi.Tests\DuoApi.Tests.csproj", "{A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DuoApi.Examples", "DuoApi.Examples\DuoApi.Examples.csproj", "{834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -20,18 +20,18 @@ Global
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}.Release|Any CPU.Build.0 = Release|Any CPU
- {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}.Release|Any CPU.Build.0 = Release|Any CPU
- {C089A10B-646D-407E-A2B8-848C6C522B13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C089A10B-646D-407E-A2B8-848C6C522B13}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C089A10B-646D-407E-A2B8-848C6C522B13}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C089A10B-646D-407E-A2B8-848C6C522B13}.Release|Any CPU.Build.0 = Release|Any CPU
+ {83919561-1430-4928-B231-02A77D8B9AD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {83919561-1430-4928-B231-02A77D8B9AD3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {83919561-1430-4928-B231-02A77D8B9AD3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {83919561-1430-4928-B231-02A77D8B9AD3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A0508DF1-F0F6-4A5E-B1F8-015D8C37E165}.Release|Any CPU.Build.0 = Release|Any CPU
+ {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {834D5CD4-19F2-4434-A35C-9D6C3FBCED1B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/duo_api_csharp/AssemblyInfo.cs b/duo_api_csharp/AssemblyInfo.cs
deleted file mode 100644
index 75fbbb2..0000000
--- a/duo_api_csharp/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("duo_api_csharp")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("duo_api_csharp")]
-[assembly: AssemblyCopyright("Copyright © 2022")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("b15c44a4-74d6-45b7-8a30-a313c2818083")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
-
-// Allow tests to access internal methods for easier testing
-[assembly: InternalsVisibleTo("DuoApiTest")]
diff --git a/duo_api_csharp/duo_api_csharp.csproj b/duo_api_csharp/duo_api_csharp.csproj
deleted file mode 100644
index 86f8272..0000000
--- a/duo_api_csharp/duo_api_csharp.csproj
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
- Debug
- AnyCPU
- {6E96C9D9-0825-4D26-83C7-8A62180F8FB9}
- Library
- false
- ClassLibrary
- v4.8
- 512
-
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
- false
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
- false
-
-
- duo_api_csharp
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/examples/App.config b/examples/App.config
deleted file mode 100644
index 193aecc..0000000
--- a/examples/App.config
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/examples/Examples.csproj b/examples/Examples.csproj
deleted file mode 100644
index 35bc283..0000000
--- a/examples/Examples.csproj
+++ /dev/null
@@ -1,59 +0,0 @@
-
-
-
-
- Debug
- AnyCPU
- {C089A10B-646D-407E-A2B8-848C6C522B13}
- Exe
- Examples
- Examples
- v4.8
- 512
- true
- true
-
-
- AnyCPU
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- AnyCPU
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {6e96c9d9-0825-4d26-83c7-8a62180f8fb9}
- duo_api_csharp
-
-
-
-
\ No newline at end of file
diff --git a/examples/Properties/AssemblyInfo.cs b/examples/Properties/AssemblyInfo.cs
deleted file mode 100644
index 6a556c3..0000000
--- a/examples/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Examples")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Examples")]
-[assembly: AssemblyCopyright("Copyright © 2022")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("c089a10b-646d-407e-a2b8-848c6c522b13")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/DuoApiTest.csproj b/test/DuoApiTest.csproj
deleted file mode 100644
index 9aaa12f..0000000
--- a/test/DuoApiTest.csproj
+++ /dev/null
@@ -1,125 +0,0 @@
-
-
-
-
-
-
- Debug
- AnyCPU
- {6B97B9FB-E553-494C-BD50-4BF7DB5C2184}
- Library
- Properties
- DuoApiTest
- DuoApiTest
- v4.8
- 512
-
-
-
-
-
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
- false
-
-
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
- false
-
-
-
- ..\packages\Microsoft.Bcl.AsyncInterfaces.6.0.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll
-
-
-
- ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll
-
-
- ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll
-
-
-
- ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll
-
-
- ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll
-
-
- ..\packages\System.Text.Encodings.Web.6.0.0\lib\net461\System.Text.Encodings.Web.dll
-
-
- ..\packages\System.Text.Json.6.0.2\lib\net461\System.Text.Json.dll
-
-
- ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll
-
-
- ..\packages\System.ValueTuple.4.5.0\lib\net47\System.ValueTuple.dll
-
-
-
-
-
-
- ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll
-
-
- ..\packages\xunit.assert.2.4.1\lib\netstandard1.1\xunit.assert.dll
-
-
- ..\packages\xunit.extensibility.core.2.4.1\lib\net452\xunit.core.dll
-
-
- ..\packages\xunit.extensibility.execution.2.4.1\lib\net452\xunit.execution.desktop.dll
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {6e96c9d9-0825-4d26-83c7-8a62180f8fb9}
- duo_api_csharp
-
-
-
-
-
- This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/test/Makefile b/test/Makefile
deleted file mode 100644
index 4d6e7b5..0000000
--- a/test/Makefile
+++ /dev/null
@@ -1,10 +0,0 @@
-DLLS := SigningTest.dll QueryParamsTest.dll
-
-test: $(DLLS)
- nunit-console $^
-
-%.dll: %.cs ../Duo.cs
- dmcs /target:library -r:System.Web.Services -r:System.Web.Extensions -r:System.Web -r:nunit.framework.dll $< ../Duo.cs -out:$@
-
-clean:
- rm -f $(DLLS) *~ TestResult.xml
diff --git a/test/Properties/AssemblyInfo.cs b/test/Properties/AssemblyInfo.cs
deleted file mode 100644
index 80f68e1..0000000
--- a/test/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("DuoApiTest")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("Cisco Systems")]
-[assembly: AssemblyProduct("DuoApiTest")]
-[assembly: AssemblyCopyright("Copyright © 2022")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("ee1b0852-a526-4b1f-bbda-178c88e8e2fa")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/test/app.config b/test/app.config
deleted file mode 100644
index 1696df6..0000000
--- a/test/app.config
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/test/packages.config b/test/packages.config
deleted file mode 100644
index 9462602..0000000
--- a/test/packages.config
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file