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 3f8212b..0c0583d 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; + } + + /// + /// 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 ValidateIdTokenFromResponse(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,48 @@ 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 ValidateIdTokenFromResponse(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); + + try + { + // Calling this method to validate the token, before getting the samlResponse value + ValidateIdTokenFromResponse(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/DuoUniversal.csproj b/DuoUniversal/DuoUniversal.csproj index 9beb6dd..0931a4d 100644 --- a/DuoUniversal/DuoUniversal.csproj +++ b/DuoUniversal/DuoUniversal.csproj @@ -6,7 +6,7 @@ netstandard2.0;net471 DuoUniversal - 1.2.5 + 1.2.6 Duo Security Duo Security Cisco Systems, Inc. and/or its affiliates 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