Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding a method for exchanging 2FA token to the SAML response #44

Merged
merged 13 commits into from
Jul 26, 2024
Merged
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
Loading