From dac777a2bacc72e6dc2f6303d3239299035512f9 Mon Sep 17 00:00:00 2001 From: Yevgen Kreshchenko <147668911+yevgenkre@users.noreply.github.com> Date: Mon, 29 Apr 2024 15:37:51 -0400 Subject: [PATCH] Issuer parameter added to the GenerateAuthUri method (#40) * .NET Framework 471 architecture added. Version bump to 1.2.3 * adding issuer parameter to the auth jwt to handle the Epic samlResponse generation * version bump to 1.2.4 * added unit tests and issuer validation * end of lines fixed * extra whitespace removed * Moving the issuer parameter to the ClientBuilder and renaming it to audienceIssuer --- DuoUniversal.Tests/TestGenerateAuthUrl.cs | 29 ++++++++++++++++++++ DuoUniversal/Client.cs | 32 ++++++++++++++++++++--- DuoUniversal/DuoUniversal.csproj | 2 +- DuoUniversal/Labels.cs | 1 + 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/DuoUniversal.Tests/TestGenerateAuthUrl.cs b/DuoUniversal.Tests/TestGenerateAuthUrl.cs index 0f78956..c62a58d 100644 --- a/DuoUniversal.Tests/TestGenerateAuthUrl.cs +++ b/DuoUniversal.Tests/TestGenerateAuthUrl.cs @@ -30,6 +30,35 @@ public void TestSuccess(string username) Assert.True(authUri.StartsWith($"https://{API_HOST}")); } + [Test] + [TestCase(USERNAME)] + [TestCase("I iz a user")] + [TestCase("user@foo.bar")] + public void TestSuccessWithIssuer(string username) + { + Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceIssuer("http://issuer").Build(); + string authUri = clientWithIssuer.GenerateAuthUri(username, STATE); + Assert.True(Uri.IsWellFormedUriString(authUri, UriKind.Absolute)); + Assert.True(authUri.StartsWith($"https://{API_HOST}")); + } + + [Test] + [TestCase(" ")] + public void TestInvalidIssuer(string issuer) + { + Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceIssuer(issuer).Build(); + Assert.Throws(() => clientWithIssuer.GenerateAuthUri("username", STATE)); + } + + [Test] + [TestCase(null)] + public void TestNullIssuer(string issuer) + { + Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceIssuer(issuer).Build(); + string authUri = clientWithIssuer.GenerateAuthUri("username", STATE); + Assert.True(Uri.IsWellFormedUriString(authUri, UriKind.Absolute)); + } + [Test] [TestCase(null)] [TestCase("")] diff --git a/DuoUniversal/Client.cs b/DuoUniversal/Client.cs index d26c1f6..3846922 100644 --- a/DuoUniversal/Client.cs +++ b/DuoUniversal/Client.cs @@ -39,6 +39,8 @@ public class Client internal bool UseDuoCodeAttribute { get; set; } = false; + internal string AudienceIssuer { get; set; } = null; + internal Client() { } @@ -83,7 +85,7 @@ public async Task DoHealthCheck(bool handleException = true) /// A URL to redirect the user's browser to public string GenerateAuthUri(string username, string state) { - ValidateAuthUriInputs(username, state); + ValidateAuthUriInputs(username, state, AudienceIssuer); string authEndpoint = CustomizeApiUri(AUTH_ENDPOINT); @@ -163,7 +165,7 @@ private string CustomizeApiUri(string baseUrl) /// /// The username to check /// The state value to check - private void ValidateAuthUriInputs(string username, string state) + private void ValidateAuthUriInputs(string username, string state, string issuer) { if (string.IsNullOrWhiteSpace(username)) { @@ -174,6 +176,10 @@ private void ValidateAuthUriInputs(string username, string state) { throw new DuoException($"state must be a non-empty string between {MINIMUM_STATE_LENGTH} and {MAXIMUM_STATE_LENGTH}."); } + if (issuer != null && issuer.Trim().Length == 0) + { + throw new DuoException("issuer can be null, but cannot be an empty string"); + } } /// @@ -196,6 +202,12 @@ private string GenerateAuthJwt(string username, string state, string authEndpoin // TODO support nonce }; + // issuer parameter is used for the Epic Hyperdrive integration only + if (AudienceIssuer != null) + { + additionalClaims[Labels.AUDIENCE_ISSUER] = AudienceIssuer; + } + if (UseDuoCodeAttribute) { additionalClaims[Labels.USE_DUO_CODE_ATTRIBUTE] = "true"; @@ -299,6 +311,7 @@ public class ClientBuilder private bool _sslCertValidation = true; private X509Certificate2Collection _customRoots = null; private IWebProxy proxy = null; + private string _audienceIssuer = null; // For testing only @@ -411,6 +424,18 @@ public ClientBuilder UseHttpProxy(IWebProxy proxy) return this; } + /// + /// Set an audienceIssuer value to generate a SAML response for the Epic integration + /// + /// Specific parameter for the Epic integration for the SAML response generation + /// The ClientBuilder + public ClientBuilder UseAudienceIssuer(string audienceIssuer) + { + _audienceIssuer = audienceIssuer; + + return this; + } + /// /// Build the Client based on the settings provided to the Builder /// @@ -425,7 +450,8 @@ public Client Build() ClientSecret = _clientSecret, ApiHost = _apiHost, RedirectUri = _redirectUri, - UseDuoCodeAttribute = _useDuoCodeAttribute + UseDuoCodeAttribute = _useDuoCodeAttribute, + AudienceIssuer = _audienceIssuer }; var httpClient = BuildHttpClient(); diff --git a/DuoUniversal/DuoUniversal.csproj b/DuoUniversal/DuoUniversal.csproj index 10316b3..2152e34 100644 --- a/DuoUniversal/DuoUniversal.csproj +++ b/DuoUniversal/DuoUniversal.csproj @@ -6,7 +6,7 @@ netstandard2.0;net471 DuoUniversal - 1.2.3 + 1.2.4 Duo Security Duo Security Cisco Systems, Inc. and/or its affiliates diff --git a/DuoUniversal/Labels.cs b/DuoUniversal/Labels.cs index 9aa6633..0ce7d33 100644 --- a/DuoUniversal/Labels.cs +++ b/DuoUniversal/Labels.cs @@ -41,5 +41,6 @@ internal class Labels public const string DUO_UNAME = "duo_uname"; public const string PREFERRED_USERNAME = "preferred_username"; public const string USE_DUO_CODE_ATTRIBUTE = "use_duo_code_attribute"; + public const string AUDIENCE_ISSUER = "issuer"; } }