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