Skip to content

Commit

Permalink
Adding a method for exchanging 2FA token to the SAML response (#44)
Browse files Browse the repository at this point in the history
* .NET Framework 471 architecture added. Version bump to 1.2.3

* renaming the audience for saml response parameter

* fixing the wrong casing

* version bump

* samlResponse added to the TokenResponse

* bumping version to 1.3

* version update, method name update

* fixing the linter formatting error
  • Loading branch information
yevgenkre authored Jul 26, 2024
1 parent ca9d9a2 commit 4a5042a
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 7 deletions.
33 changes: 32 additions & 1 deletion DuoUniversal.Tests/TestExchangeCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<DuoException>(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<DuoException>(async () => await client.ExchangeAuthorizationCodeForSamlResponse(CODE, username));
}

private static string GoodApiResponse()
{
var responseValues = new Dictionary<string, string>
Expand All @@ -73,5 +91,18 @@ private static string GoodApiResponse()
};
return JsonSerializer.Serialize(responseValues);
}

private static string GoodApiResponseWithSamlResponse()
{
var responseValues = new Dictionary<string, string>
{
{"access_token", "access token"},
{"expires_in", "1"},
{"id_token", CreateTokenJwt()},
{"token_type", "Bearer"},
{"saml_response", "saml_response"}
};
return JsonSerializer.Serialize(responseValues);
}
}
}
62 changes: 57 additions & 5 deletions DuoUniversal/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,12 @@ public string GenerateAuthUri(string username, string state)
}

/// <summary>
/// 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.
/// </summary>
/// <param name="duoCode">The one-time use code issued by Duo</param>
/// <param name="username">The username expected to have authenticated with Duo</param>
/// <returns>An IdToken authenticating the user and describing the authentication</returns>
public async Task<IdToken> ExchangeAuthorizationCodeFor2faResult(string duoCode, string username)
/// <returns>A TokenResponse authenticating the user and describing the authentication</returns>
private async Task<TokenResponse> ExchangeAuthorizationCodeResponse(string duoCode)
{
string tokenEndpoint = CustomizeApiUri(TOKEN_ENDPOINT);

Expand All @@ -128,9 +126,21 @@ public async Task<IdToken> ExchangeAuthorizationCodeFor2faResult(string duoCode,
throw new DuoException("Error exchanging the code for a 2fa token", e);
}

return tokenResponse;
}

/// <summary>
/// Extracts and validates the Id Token from the response.
/// Will raise a DuoException if the username does not match the Id Token.
/// </summary>
/// <param name="duoCode">The one-time use code issued by Duo</param>
/// <returns>A TokenResponse authenticating the user and describing the authentication</returns>
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);
}
Expand All @@ -148,6 +158,48 @@ public async Task<IdToken> ExchangeAuthorizationCodeFor2faResult(string duoCode,
}


/// <summary>
/// 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.
/// </summary>
/// <param name="duoCode">The one-time use code issued by Duo</param>
/// <param name="username">The username expected to have authenticated with Duo</param>
/// <returns>An IdToken authenticating the user and describing the authentication</returns>
public async Task<IdToken> ExchangeAuthorizationCodeFor2faResult(string duoCode, string username)
{
TokenResponse tokenResponse = await ExchangeAuthorizationCodeResponse(duoCode);
return ValidateIdTokenFromResponse(tokenResponse, username);

}

/// <summary>
/// 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.
/// </summary>
/// <param name="duoCode">The one-time use code issued by Duo</param>
/// <param name="username">The username expected to have authenticated with Duo</param>
/// <returns>A string authenticating the user and containing the saml response</returns>
public async Task<string> 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;
}


/// <summary>
/// Customize a URI template based on the Duo API Host value
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion DuoUniversal/DuoUniversal.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net471</TargetFrameworks>
<PackageId>DuoUniversal</PackageId>
<Version>1.2.5</Version>
<Version>1.2.6</Version>
<Authors>Duo Security</Authors>
<Company>Duo Security</Company>
<Copyright>Cisco Systems, Inc. and/or its affiliates</Copyright>
Expand Down
2 changes: 2 additions & 0 deletions DuoUniversal/Models.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 4a5042a

Please sign in to comment.