From c0165b8e6369942f1aad4c747509d5debfa87c7d Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Wed, 10 Apr 2024 16:27:32 -0400 Subject: [PATCH 1/8] .NET Framework 471 architecture added. Version bump to 1.2.3 --- DuoUniversal/DuoUniversal.csproj | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/DuoUniversal/DuoUniversal.csproj b/DuoUniversal/DuoUniversal.csproj index 194b04b..10316b3 100644 --- a/DuoUniversal/DuoUniversal.csproj +++ b/DuoUniversal/DuoUniversal.csproj @@ -4,9 +4,9 @@ - netstandard2.0 + netstandard2.0;net471 DuoUniversal - 1.2.2 + 1.2.3 Duo Security Duo Security Cisco Systems, Inc. and/or its affiliates @@ -17,6 +17,7 @@ git Duo two-factor authentication for .NET web applications https://github.com/duosecurity/duo_universal_csharp + true @@ -25,6 +26,10 @@ + + + + <_Parameter1>DuoUniversal.Tests From 78ae515886224666b01129e6a43eeae066d8688b Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Thu, 6 Jun 2024 14:15:02 -0400 Subject: [PATCH 2/8] renaming the audience for saml response parameter --- DuoUniversal.Tests/TestGenerateAuthUrl.cs | 6 +++--- DuoUniversal/Client.cs | 16 ++++++++-------- DuoUniversal/DuoUniversal.csproj | 4 ++-- DuoUniversal/Labels.cs | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/DuoUniversal.Tests/TestGenerateAuthUrl.cs b/DuoUniversal.Tests/TestGenerateAuthUrl.cs index c62a58d..1ae60c7 100644 --- a/DuoUniversal.Tests/TestGenerateAuthUrl.cs +++ b/DuoUniversal.Tests/TestGenerateAuthUrl.cs @@ -36,7 +36,7 @@ public void TestSuccess(string username) [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(); + Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceForSamlResponse("http://issuer").Build(); string authUri = clientWithIssuer.GenerateAuthUri(username, STATE); Assert.True(Uri.IsWellFormedUriString(authUri, UriKind.Absolute)); Assert.True(authUri.StartsWith($"https://{API_HOST}")); @@ -46,7 +46,7 @@ public void TestSuccessWithIssuer(string username) [TestCase(" ")] public void TestInvalidIssuer(string issuer) { - Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceIssuer(issuer).Build(); + Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceForSamlResponse(issuer).Build(); Assert.Throws(() => clientWithIssuer.GenerateAuthUri("username", STATE)); } @@ -54,7 +54,7 @@ public void TestInvalidIssuer(string issuer) [TestCase(null)] public void TestNullIssuer(string issuer) { - Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceIssuer(issuer).Build(); + Client clientWithIssuer = new ClientBuilder(CLIENT_ID, CLIENT_SECRET, API_HOST, REDIRECT_URI).UseAudienceForSamlResponse(issuer).Build(); string authUri = clientWithIssuer.GenerateAuthUri("username", STATE); Assert.True(Uri.IsWellFormedUriString(authUri, UriKind.Absolute)); } diff --git a/DuoUniversal/Client.cs b/DuoUniversal/Client.cs index 3846922..fd71a95 100644 --- a/DuoUniversal/Client.cs +++ b/DuoUniversal/Client.cs @@ -39,7 +39,7 @@ public class Client internal bool UseDuoCodeAttribute { get; set; } = false; - internal string AudienceIssuer { get; set; } = null; + internal string audience_for_saml_response { get; set; } = null; internal Client() { @@ -85,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, AudienceIssuer); + ValidateAuthUriInputs(username, state, audience_for_saml_response); string authEndpoint = CustomizeApiUri(AUTH_ENDPOINT); @@ -203,9 +203,9 @@ private string GenerateAuthJwt(string username, string state, string authEndpoin }; // issuer parameter is used for the Epic Hyperdrive integration only - if (AudienceIssuer != null) + if (audience_for_saml_response != null) { - additionalClaims[Labels.AUDIENCE_ISSUER] = AudienceIssuer; + additionalClaims[Labels.AUDIENCE_FOR_SAML_RESPONSE] = audience_for_saml_response; } if (UseDuoCodeAttribute) @@ -311,7 +311,7 @@ public class ClientBuilder private bool _sslCertValidation = true; private X509Certificate2Collection _customRoots = null; private IWebProxy proxy = null; - private string _audienceIssuer = null; + private string _audienceForSamlResponse = null; // For testing only @@ -429,9 +429,9 @@ public ClientBuilder UseHttpProxy(IWebProxy proxy) /// /// Specific parameter for the Epic integration for the SAML response generation /// The ClientBuilder - public ClientBuilder UseAudienceIssuer(string audienceIssuer) + public ClientBuilder UseAudienceForSamlResponse(string audienceIssuer) { - _audienceIssuer = audienceIssuer; + _audienceForSamlResponse = audienceIssuer; return this; } @@ -451,7 +451,7 @@ public Client Build() ApiHost = _apiHost, RedirectUri = _redirectUri, UseDuoCodeAttribute = _useDuoCodeAttribute, - AudienceIssuer = _audienceIssuer + audience_for_saml_response = _audienceForSamlResponse }; var httpClient = BuildHttpClient(); diff --git a/DuoUniversal/DuoUniversal.csproj b/DuoUniversal/DuoUniversal.csproj index 2152e34..2bff890 100644 --- a/DuoUniversal/DuoUniversal.csproj +++ b/DuoUniversal/DuoUniversal.csproj @@ -26,9 +26,9 @@ - + - + diff --git a/DuoUniversal/Labels.cs b/DuoUniversal/Labels.cs index 0ce7d33..908eb7d 100644 --- a/DuoUniversal/Labels.cs +++ b/DuoUniversal/Labels.cs @@ -41,6 +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"; + public const string AUDIENCE_FOR_SAML_RESPONSE = "audience_for_saml_response"; } } From dde6eca987d4dcba281ce66ee6954b1ee5eadad7 Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Fri, 7 Jun 2024 10:59:16 -0400 Subject: [PATCH 3/8] fixing the wrong casing --- DuoUniversal/Client.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/DuoUniversal/Client.cs b/DuoUniversal/Client.cs index fd71a95..6acec8f 100644 --- a/DuoUniversal/Client.cs +++ b/DuoUniversal/Client.cs @@ -39,7 +39,7 @@ public class Client internal bool UseDuoCodeAttribute { get; set; } = false; - internal string audience_for_saml_response { get; set; } = null; + internal string AudienceForSamlResponse { get; set; } = null; internal Client() { @@ -85,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, audience_for_saml_response); + ValidateAuthUriInputs(username, state, AudienceForSamlResponse); string authEndpoint = CustomizeApiUri(AUTH_ENDPOINT); @@ -203,9 +203,9 @@ private string GenerateAuthJwt(string username, string state, string authEndpoin }; // issuer parameter is used for the Epic Hyperdrive integration only - if (audience_for_saml_response != null) + if (AudienceForSamlResponse != null) { - additionalClaims[Labels.AUDIENCE_FOR_SAML_RESPONSE] = audience_for_saml_response; + additionalClaims[Labels.AUDIENCE_FOR_SAML_RESPONSE] = AudienceForSamlResponse; } if (UseDuoCodeAttribute) @@ -425,13 +425,13 @@ public ClientBuilder UseHttpProxy(IWebProxy proxy) } /// - /// Set an audienceIssuer value to generate a SAML response for the Epic integration + /// Set an audienceForSamlResponse value to generate a SAML response for the Epic integration /// - /// Specific parameter for the Epic integration for the SAML response generation + /// Specific parameter for the Epic integration for the SAML response generation /// The ClientBuilder - public ClientBuilder UseAudienceForSamlResponse(string audienceIssuer) + public ClientBuilder UseAudienceForSamlResponse(string audienceForSamlResponse) { - _audienceForSamlResponse = audienceIssuer; + _audienceForSamlResponse = audienceForSamlResponse; return this; } @@ -451,7 +451,7 @@ public Client Build() ApiHost = _apiHost, RedirectUri = _redirectUri, UseDuoCodeAttribute = _useDuoCodeAttribute, - audience_for_saml_response = _audienceForSamlResponse + AudienceForSamlResponse = _audienceForSamlResponse }; var httpClient = BuildHttpClient(); From 26e0f794251e48b3a2341cff5f0583d5c80471c4 Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Tue, 11 Jun 2024 11:55:03 -0400 Subject: [PATCH 4/8] version bump --- DuoUniversal/DuoUniversal.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuoUniversal/DuoUniversal.csproj b/DuoUniversal/DuoUniversal.csproj index 2bff890..9beb6dd 100644 --- a/DuoUniversal/DuoUniversal.csproj +++ b/DuoUniversal/DuoUniversal.csproj @@ -6,7 +6,7 @@ netstandard2.0;net471 DuoUniversal - 1.2.4 + 1.2.5 Duo Security Duo Security Cisco Systems, Inc. and/or its affiliates From 9ec8ebbb1ddad33751a1be3cd55e98a7e9f5de44 Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Wed, 10 Jul 2024 20:59:34 -0400 Subject: [PATCH 5/8] samlResponse added to the TokenResponse --- DuoUniversal.Tests/TestExchangeCode.cs | 33 +++++++++++++- DuoUniversal/Client.cs | 61 +++++++++++++++++++++++--- DuoUniversal/Models.cs | 2 + 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/DuoUniversal.Tests/TestExchangeCode.cs b/DuoUniversal.Tests/TestExchangeCode.cs index 0e23933..78f1a2b 100644 --- a/DuoUniversal.Tests/TestExchangeCode.cs +++ b/DuoUniversal.Tests/TestExchangeCode.cs @@ -31,6 +31,15 @@ public async Task TestSuccess() Assert.AreEqual(idToken.Username, USERNAME); } + [Test] + public async Task TestSamlResponseSuccess() + { + string goodResponse = GoodApiResponseWithSamlResponse(); + var client = MakeClient(new HttpResponder(HttpStatusCode.OK, new StringContent(goodResponse))); + string samlResponse = await client.ExchangeAuthorizationCodeForSamlResponse(CODE, USERNAME); + Assert.NotNull(samlResponse); + } + [Test] [TestCase(HttpStatusCode.MovedPermanently)] // 301 [TestCase(HttpStatusCode.BadRequest)] // 400 @@ -56,12 +65,21 @@ public void TestHttpException() [TestCase("!@#user$%^name*&(")] public void TestUsernameMismatch(string username) { - // Will have the USERNAME specified above + // Will have the USERNAME specified in the parent class string goodResponse = GoodApiResponse(); var client = MakeClient(new HttpResponder(HttpStatusCode.OK, new StringContent(goodResponse))); Assert.ThrowsAsync(async () => await client.ExchangeAuthorizationCodeFor2faResult(CODE, username)); } + [Test] + [TestCase("not username")] + public void TestUsernameMismatchSamlResponseFailure(string username) + { + string goodResponse = GoodApiResponseWithSamlResponse(); + var client = MakeClient(new HttpResponder(HttpStatusCode.OK, new StringContent(goodResponse))); + Assert.ThrowsAsync(async () => await client.ExchangeAuthorizationCodeForSamlResponse(CODE, username)); + } + private static string GoodApiResponse() { var responseValues = new Dictionary @@ -73,5 +91,18 @@ private static string GoodApiResponse() }; return JsonSerializer.Serialize(responseValues); } + + private static string GoodApiResponseWithSamlResponse() + { + var responseValues = new Dictionary + { + {"access_token", "access token"}, + {"expires_in", "1"}, + {"id_token", CreateTokenJwt()}, + {"token_type", "Bearer"}, + {"saml_response", "saml_response"} + }; + return JsonSerializer.Serialize(responseValues); + } } } diff --git a/DuoUniversal/Client.cs b/DuoUniversal/Client.cs index 6acec8f..c9ffb2c 100644 --- a/DuoUniversal/Client.cs +++ b/DuoUniversal/Client.cs @@ -95,14 +95,12 @@ public string GenerateAuthUri(string username, string state) } /// - /// Send the authorization code provided by Duo back to Duo in exchange for an Id Token authenticating the user and - /// providing details about the authentication. + /// Send the authorization code provided by Duo back to Duo in exchange for a full Duo response. /// Will raise a DuoException if the username does not match the Id Token. /// /// The one-time use code issued by Duo - /// The username expected to have authenticated with Duo - /// An IdToken authenticating the user and describing the authentication - public async Task ExchangeAuthorizationCodeFor2faResult(string duoCode, string username) + /// A TokenResponse authenticating the user and describing the authentication + private async Task ExchangeAuthorizationCodeResponse(string duoCode) { string tokenEndpoint = CustomizeApiUri(TOKEN_ENDPOINT); @@ -128,9 +126,21 @@ public async Task ExchangeAuthorizationCodeFor2faResult(string duoCode, throw new DuoException("Error exchanging the code for a 2fa token", e); } + return tokenResponse; + } + + /// + /// Send the authorization code provided by Duo back to Duo in exchange for a full Duo response. + /// Will raise a DuoException if the username does not match the Id Token. + /// + /// The one-time use code issued by Duo + /// A TokenResponse authenticating the user and describing the authentication + private IdToken IdTokenFromResponse(TokenResponse tokenResponse, string username) + { IdToken idToken; try { + string tokenEndpoint = CustomizeApiUri(TOKEN_ENDPOINT); JwtUtils.ValidateJwt(tokenResponse.IdToken, ClientId, ClientSecret, tokenEndpoint); idToken = Utils.DecodeToken(tokenResponse.IdToken); } @@ -148,6 +158,47 @@ public async Task ExchangeAuthorizationCodeFor2faResult(string duoCode, } + /// + /// Send the authorization code provided by Duo back to Duo in exchange for an Id Token authenticating the user and + /// providing details about the authentication. + /// Will raise a DuoException if the username does not match the Id Token. + /// + /// The one-time use code issued by Duo + /// The username expected to have authenticated with Duo + /// An IdToken authenticating the user and describing the authentication + public async Task ExchangeAuthorizationCodeFor2faResult(string duoCode, string username) + { + TokenResponse tokenResponse = await ExchangeAuthorizationCodeResponse(duoCode); + return IdTokenFromResponse(tokenResponse, username); + + } + + /// + /// Send the authorization code provided by Duo back to Duo in exchange for an SAML response, used for some integrations. + /// Will raise a DuoException if the username does not match the Id Token. + /// + /// The one-time use code issued by Duo + /// The username expected to have authenticated with Duo + /// A string authenticating the user and containing the saml response + public async Task ExchangeAuthorizationCodeForSamlResponse(string duoCode, string username) + { + string samlResponse; + TokenResponse tokenResponse = await ExchangeAuthorizationCodeResponse(duoCode); + //checking if the IdToken valid before assigning saml response + try + { + IdTokenFromResponse(tokenResponse, username); + samlResponse = tokenResponse.SamlResponse; + } + catch (Exception e) + { + throw new DuoException("Error while retrieveing saml response", e); + } + + return samlResponse; + } + + /// /// Customize a URI template based on the Duo API Host value /// diff --git a/DuoUniversal/Models.cs b/DuoUniversal/Models.cs index 1adce0b..d3fa958 100644 --- a/DuoUniversal/Models.cs +++ b/DuoUniversal/Models.cs @@ -38,6 +38,8 @@ internal class TokenResponse public string TokenType { get; set; } [JsonPropertyName("expires_in")] public int ExpiresIn { get; set; } + [JsonPropertyName("saml_response")] + public string SamlResponse { get; set; } } public class IdToken From 31760b1eeebea83bf7d190e8d0b54b801c68050d Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Wed, 10 Jul 2024 21:04:36 -0400 Subject: [PATCH 6/8] bumping version to 1.3 --- DuoUniversal/DuoUniversal.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuoUniversal/DuoUniversal.csproj b/DuoUniversal/DuoUniversal.csproj index 9beb6dd..2b4869a 100644 --- a/DuoUniversal/DuoUniversal.csproj +++ b/DuoUniversal/DuoUniversal.csproj @@ -6,7 +6,7 @@ netstandard2.0;net471 DuoUniversal - 1.2.5 + 1.3.0 Duo Security Duo Security Cisco Systems, Inc. and/or its affiliates From b8cf2aac4d74b6b15a3d8ddc49a35a6a8d20ca6d Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Thu, 25 Jul 2024 16:29:16 -0400 Subject: [PATCH 7/8] version update, method name update --- DuoUniversal/Client.cs | 11 ++++++----- DuoUniversal/DuoUniversal.csproj | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/DuoUniversal/Client.cs b/DuoUniversal/Client.cs index 0444f96..af620de 100644 --- a/DuoUniversal/Client.cs +++ b/DuoUniversal/Client.cs @@ -130,12 +130,12 @@ private async Task ExchangeAuthorizationCodeResponse(string duoCo } /// - /// Send the authorization code provided by Duo back to Duo in exchange for a full Duo response. + /// Extracts and validates the Id Token from the response. /// Will raise a DuoException if the username does not match the Id Token. /// /// The one-time use code issued by Duo /// A TokenResponse authenticating the user and describing the authentication - private IdToken IdTokenFromResponse(TokenResponse tokenResponse, string username) + private IdToken ValidateIdTokenFromResponse(TokenResponse tokenResponse, string username) { IdToken idToken; try @@ -169,7 +169,7 @@ private IdToken IdTokenFromResponse(TokenResponse tokenResponse, string username public async Task ExchangeAuthorizationCodeFor2faResult(string duoCode, string username) { TokenResponse tokenResponse = await ExchangeAuthorizationCodeResponse(duoCode); - return IdTokenFromResponse(tokenResponse, username); + return ValidateIdTokenFromResponse(tokenResponse, username); } @@ -184,10 +184,11 @@ public async Task ExchangeAuthorizationCodeForSamlResponse(string duoCod { string samlResponse; TokenResponse tokenResponse = await ExchangeAuthorizationCodeResponse(duoCode); - //checking if the IdToken valid before assigning saml response + try { - IdTokenFromResponse(tokenResponse, username); + // Calling this method to validate the token, before getting the samlResponse value + ValidateIdTokenFromResponse(tokenResponse, username); samlResponse = tokenResponse.SamlResponse; } catch (Exception e) diff --git a/DuoUniversal/DuoUniversal.csproj b/DuoUniversal/DuoUniversal.csproj index 2b4869a..0931a4d 100644 --- a/DuoUniversal/DuoUniversal.csproj +++ b/DuoUniversal/DuoUniversal.csproj @@ -6,7 +6,7 @@ netstandard2.0;net471 DuoUniversal - 1.3.0 + 1.2.6 Duo Security Duo Security Cisco Systems, Inc. and/or its affiliates From 27352bdd3748a17efbf5cf22f3f62364517301a6 Mon Sep 17 00:00:00 2001 From: "Yevgen Kreshchenko (ykreshch)" Date: Thu, 25 Jul 2024 16:49:42 -0400 Subject: [PATCH 8/8] fixing the linter formatting error --- DuoUniversal/Client.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DuoUniversal/Client.cs b/DuoUniversal/Client.cs index af620de..0c0583d 100644 --- a/DuoUniversal/Client.cs +++ b/DuoUniversal/Client.cs @@ -184,7 +184,7 @@ public async Task ExchangeAuthorizationCodeForSamlResponse(string duoCod { string samlResponse; TokenResponse tokenResponse = await ExchangeAuthorizationCodeResponse(duoCode); - + try { // Calling this method to validate the token, before getting the samlResponse value