Duo recently released their new Universal two factor authentication method which changes from an iFrame method to using a redirect. Duo provides a C# example using .NET Core and MVC but if you still have old web forms applications you’ve built and aren’t ready, you can still integrate them with Duo Universal.
First, you’ll need to buil the DuoUniversal.dll binary from the project https://github.com/duosecurity/duo_universal_csharp and add it as a reference to your project. You’ll also need to make sure you’re running .NET Framework 4.7.1 or higher for your project.
Second, add the following items from Nuget to the project
- System.Text.Json
- System.Net.Http.Json
- Microsoft.IdentityModel.Tokens
- Microsoft.IdentityModel.JsonWebTokens
Now you’re ready to start integrating it into your current project.
- On your login page, add Using DuoUniversal
- Declare string values for Duo Settings
string ClientId = ""; //Setting from Duo
string ClientSecret = ""; //Setting from Duo
string ApiHost = ""; //Setting from Duo
string RedirectUri = "https://localhost:44310/Redirect.aspx"; //This is the URL you will redirect to after authentication. It does not need to be publically accessible.
- Create a new method InitiateDuo
public async void InitiateDuo(string User)
{
//Create client using config settings
Client duoClient = new DuoUniversal.ClientBuilder(ClientId, ClientSecret, ApiHost, RedirectUri).Build();
Session["client"] = duoClient;
// Check if Duo seems to be healthy and able to service authentications.
// If Duo were unhealthy, you could possibly send user to an error page, or implement a fail mode
var isDuoHealthy = await duoClient.DoHealthCheck();
// Generate a random state value to tie the authentication steps together
string state = DuoUniversal.Client.GenerateState();
// Save the state and username in the session for later
Session["STATE_SESSION_KEY"] = state;
Session["USERNAME_SESSION_KEY"] = User;
// Get the URI of the Duo prompt from the client. This includes an embedded authentication request.
string promptUri = duoClient.GenerateAuthUri(User, state);
// Redirect the user's browser to the Duo prompt.
// The Duo prompt, after authentication, will redirect back to the configured Redirect URI to complete the authentication flow.
// In this example, that is /duo_callback, which is implemented in Callback.cshtml.cs.
Response.Redirect(promptUri,false);
}
- Add the following code to the portion where you want to trigger the Duo prompt.
InitiateDuo(User); //User should be the username trying to login.
- On your login.aspx page, add Async=”true” to the top
<%@ Page Async="true" Language="C#"......
- Create a new Redirect page which Duo will transfer the user to after authenticating with two factor. Add add Using DuoUniversal to this page.
- Create a new method DUOResponse
public async void DUOResponse(string sessionState, string sessionUsername, string receievedstate, string receivedcode)
{
Client duoClient = (Client)Session["client"];
//Confirm the original state(from the session) matches the state sent by Duo; this helps p
if (!sessionState.Equals(receievedstate))
{
throw new DuoException("Session state did not match the expected state");
}
//Session.Clear();
IdToken token = await duoClient.ExchangeAuthorizationCodeFor2faResult(receivedcode, sessionUsername);
if (token.AuthResult.Result == "allow")
{
Session["authenticated"] = true;
Response.Redirect("Home.aspx");
}
}
- In the Page_Load method of the Redirect page add the following code
string sessionState = string.Empty;
if (Session["STATE_SESSION_KEY"] != null)
sessionState = Session["STATE_SESSION_KEY"].ToString();
else
Response.Redirect("login.aspx?Msg=" + HttpUtility.UrlEncode("Duo Session State Key Missing"));
string sessionUsername = string.Empty;
if (Session["userName"] != null)
sessionUsername = Session["userName"].ToString();
else
Response.Redirect("login.aspx?Msg=" + HttpUtility.UrlEncode("Duo Session Username Missing"));
string receievedstate = string.Empty;
if (Request.QueryString["state"] != null)
receievedstate = Request.QueryString["state"];
else
Response.Redirect("login.aspx?Msg=" + HttpUtility.UrlEncode("Duo State URL Variable Missing"));
string receivedcode = string.Empty;
if (Request.QueryString["code"] != null)
receivedcode = Request.QueryString["code"];
else
Response.Redirect("login.aspx?Msg=" + HttpUtility.UrlEncode("Duo Code URL Variable Missing"));
if (string.IsNullOrEmpty(sessionState) || string.IsNullOrEmpty(sessionUsername)
|| string.IsNullOrEmpty(receievedstate) || string.IsNullOrEmpty(receivedcode))
{
Response.Redirect("login.aspx?Msg=" + HttpUtility.UrlEncode("Duo null items"));
return;
}
DUOResponse(sessionState, sessionUsername, receievedstate, receivedcode);
- On your Redirect.aspx page, add Async=”true” to the top
<%@ Page Async="true" Language="C#"......
Build and test. When you test it the first time you will see the old prompt. If everything works, you can go into Duo Admin console and turn on the new prompt.
When does ProcessDuoResponse() get called?
Thank you for bringing that to my attention. I’ve updated the page to include the correction (you may notice some name changes as I’ve adjusted the code a bit since I first published that post).
do you have that code for mvc asp.net?
Hi Caio, that’s available directly from Cisco in their GitHub repository
Very good article. Thank you for putting this out!
I don’t think that webforms is covered by Cisco/Duo in their documentation at all. There are many many web applications still running Webforms and would not prefer to move onto MVC or Core just for Duo. Thank you again!
I must say though, I wasnt able to implement this solution successfully. For some reasons, the Querystring is empty in the page load method of the Redirect page.
I have successfully built the DuoUniversal binary and can start the Example server. However, after I enter my valid email address (Duo userid) and fake password into the Duo login screen, I keep receiving this error:
{
“error: “unauthorized_request”,
“error_description”: “Unauthorized OIDC request”
}
I have triple checked the Duo Application setup and ClientID/Secret/APIHost. Duo logs show nothing. I receive the same error when calling “duoClient.DoHealthCheck” from my own application. I can’t locate any documentation of this specific error message.
Any ideas are appreciated.
I figured out the issue. I was using the Duo “Auth API” where I should have used “Web SDK”.
Glad you were able to resolve!
Hello Brandon,
Thank you for such a comprehensive presentation.
I got everything to work just fine. My cell phone receives the code which I enter into the DUO popup that appears on my login webform. I get the green arrow indicating successful login followed by a redirect. Here’s the problem. If I set string RedirectUri to https://google.com, the redirect to google is successful. But if I set string RedirectUri “https://localhost:44386/Redirect.aspx”; ( 44386 is the port I am running on) I receive the error:
The resource cannot be found.
Description: HTTP 404. The resource you are looking for (or one of its dependencies) could have been removed, had its name changed, or is temporarily unavailable. Please review the following URL and make sure that it is spelled correctly.
Requested URL: /login.aspx
Version Information: Microsoft .NET Framework Version:4.0.30319; ASP.NET Version:4.8.9206.0
Neil Gorin
Hi Neil,
That’s exactly how I had it as well, using the local port in the development build. What is weird is your message says requested URL: /login.aspx which makes me think possibly something else is up
I found the problem. I renamed your login.aspx.cs and login.aspx to Default.aspx.cs and Default.aspx respectfully. Please bold a line in your documentation not to rename any of the pages. hahaha
Neil
Glad you got it figured out!
I Brandon,
Your sample code runs flawlessly on my computer. Thank you.
So, naturally I attempted to wire up my real app for DUO 2Af using your code as example. Initially, a copied your pages verbatim into my application after installing DUO universal 1.2.0. I get ever so close to success.
The following line of code returns a ‘null’ rather that the expected ‘allow’ ;
IdToken token = await duoClient.ExchangeAuthorizationCodeFor2faResult(receivedcode, sessionUsername);
Funny though, the DUO app on my iPhone indicates success and my application proceeds to the idToken line
Hi Brandon,
Brandon,
I found the answer. I was using a legacy communication protocol in my old web form app. Please add a note to all of the legacy web form supporters to upgrade the communication protocol or things will not work correctly. With all fairness to Cisco, they mention the need to upgrade in their documentation, but I did not know how to even start until I did a deep on the subject.
I added the following using statement to my app’s initialization page.
using System.Net
Then I added the following line of code at the very top of my app’s initialization.
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
Once again, thank you for your inspiration.
Neil Gorin
PS I found the post below on the Cisco site: *Kudos* to ppedersen
(https://community.cisco.com/t5/protecting-applications/issue-integrating-duo-universal-to-asp-net/td-p/4934272)
Previously, the Client ID was called the “Integration key” and the Client secret was called the “Secret key”.
You will also need to add an assembly for net Standard in your web.config file.
add assembly=”netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51″