C# Example
C# Example
Centrify provides an example ASP.net application composed of a Visual Studio solution with two projects. The example shows a custom web application built using the Centrify REST API. The Centrify.Samples.AspNet.ApiDemo
project contains the user interface code while the Centrify.Samples.AspNet.ApiDemo
project contains implementations for common web operations (for example, a REST client that abstracts away the logic to construct and invoke a REST endpoint).
The code is available on GitHub and can be used as a starting point for creating your application.
This section provides a brief overview showing how the code performs authentication using the Centrify Identity Platform API's.
The authentication steps performed by the example application’s login screen are located in login.aspx.cs
(Centrify.Samples.AspNet.ApiDemo project) which is responsible for the login screen UI. The following shows the login screen in its initial state:
The StartAuthentication()
method in login.aspx.cs
is invoked when the user enters their name and clicks Next:
protected void StartAuthentication(object sender, EventArgs e)
{
RestClient authenticationClient = UiDrivenLogin.Authenticate(TenantUrl, UserName_TextBox.Text + DefaultDomain);
if (authenticationClient != null)
{
ProcessChallenges(authenticationClient.ChallengeCollection);
Session["AuthenticaitonClient"] = authenticationClient;
UserName_TextBox.Enabled = false;
SocialLogin_Div.Visible = false;
Register_HyperLink_Div.Visible = false;
if (authenticationClient.AllowPasswordReset)
{
ForgotPass_Button.Visible = true;
}
RememberMe_Div.Visible = true;
StartOver_Button.Visible = true;
}
else
{
//There was an error
Login_Div.Visible = false;
FailureText.Text = "There was an unexpected error. Please contact your system administrator. Click here to <a href=\"Login.aspx\">start over.</a>";
ErrorMessage.Visible = true;
}
}
This method obtains a RestClient
object from the helper class and method :UiDrivenLogin.Authenticate
.
protected void StartAuthentication(object sender, EventArgs e)
{
RestClient authenticationClient = UiDrivenLogin.Authenticate(TenantUrl, UserName_TextBox.Text + DefaultDomain);
Note: The RestClient
is stored in a Session
hash table for use in subsequent endpoint calls.
UiDrivenLogin.Authenticate
invokes the /Security/StartAuthentication
endpoint using the TenantUrl
, which the code obtains from the project’s AppSettings
resource startup:
public partial class Login : System.Web.UI.Page
{
public static string TenantUrl = ConfigurationManager.AppSettings["TenantUrl"].ToString();
.
.
UiDrivenLogin.Authenticate()
also passes in the username that the user entered in the screen’s User Name
field.
StartAuthetication()
then invokes ProcessChallenges()
, which iterates through the Challenges
collections returned, and populates a list of mechanisms from which the user can select from. The code also displays UI elements for the default selected challenge:
protected void ProcessChallenges(dynamic challengeCollection, bool doSecondChallenge = false)
{
//We should clear our dropdown list every time to ensure we are starting with a clean list.
MFAChallenge_DropDownList.Items.Clear();
// We need to satisfy one of each challenge collection:
SortedList<string, string> items = new SortedList<string, string>();
//If doSecondChallenge is true we will change the challenge set array to the second set. Otherwise, we will use the first Challenge set
int challengeSet = 0;
if (doSecondChallenge)
{
challengeSet = 1;
}
//The first step for the drop down list is to populate a SortedList with our MFA mechanisms
for (int mechIdx = 0; mechIdx < challengeCollection[challengeSet]["Mechanisms"].Count; mechIdx++)
{
string mechValue = ConvertDicToString(challengeCollection[challengeSet]["Mechanisms"][mechIdx]);
items.Add(UiDrivenLogin.MechToDescription(challengeCollection[challengeSet]["Mechanisms"][mechIdx]), mechValue);
}
//The second step for the drop down list is to bind the SortedList to our dropdown element
try
{
MFAChallenge_DropDownList.DataTextField = "Key";
MFAChallenge_DropDownList.DataValueField = "Value";
MFAChallenge_DropDownList.DataSource = items;
MFAChallenge_DropDownList.DataBind();
}
catch (Exception ex)
{
//There was an error
Login_Div.Visible = false;
FailureText.Text = ex.InnerException.ToString();
ErrorMessage.Visible = true;
}
StartAuthentication_Next_Button.Visible = false;
AdvanceAuthentication_Next_Button.Visible = true;
Form.DefaultButton = AdvanceAuthentication_Next_Button.UniqueID;
MFAChallenge_DropDownList.Visible = true;
MFAChallenge_DropDownList_Label.Visible = true;
Dictionary<string, string> mech = MFAChallenge_DropDownList.SelectedItem.Value.TrimEnd(';').Split(';').ToDictionary(item => item.Split('=')[0], item => item.Split('=')[1]);
//If our first mechanism in our dropdown has a text answer type we should show our MFA answer text box.
if (mech["AnswerType"] == "Text")
{
MFAAnswer_Label.Text = UiDrivenLogin.MechToPrompt(mech);
MFAAnswer_Label.Visible = true;
MFAAnswer_Textbox.Visible = true;
}
else if (mech["AnswerType"] != "Text" && mech["AnswerType"] != "StartTextOob")
{
//Set the Loading text for polling
MFALoad_Label.Text = MFAAnswer_Label.Text = UiDrivenLogin.MechToPrompt(mech);
}
}
After this method completes, the screen now appears as follows:
The user then enters the password and clicks Next. This invokes AdvanceAuthentication()
, which obtains the selected mechanism (a Dictionary object called mech
containing the challenge details) and the RestClient
that was stored by StartAuthentication()
.
The code then invokes UiDrivenLogin.AdvanceForMech()
, which invokes /Security/AdvanceAuthentication
. The results from UiDrivenLogin.AdvanceForMech()
are stored in resultsDictionary
. The code displays a UI where the user enters the details required to complete the mechanism. The UI depends on the type of mechanism selected by the user.
//Called when the user selects an MFA type from the MFA DropDown and presses next.
protected void AdvanceAuthentication(object sender, EventArgs e)
{
Dictionary<string, string> mech = MFAChallenge_DropDownList.SelectedItem.Value.TrimEnd(';').Split(';').ToDictionary(item => item.Split('=')[0], item => item.Split('=')[1]);
RestClient authenticationClient = (RestClient)Session["AuthenticaitonClient"];
Dictionary<string, dynamic> resultsDictionary = new Dictionary<string, dynamic>();
if (sender.Equals(ForgotPass_Button))
{
resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, null, null, false, true);
MFAAnswer_Label.Visible = false;
MFAAnswer_Textbox.Visible = false;
RememberMe_Div.Visible = false;
ForgotPass_Button_Div.Visible = false;
}
else
{
//If the mechanism is StartTextOob we need to allow the user to answer via text and poll at the same time.
if (mech["AnswerType"] == "StartTextOob")
{
if (StartTextOob_Timer.Enabled) //We have already called AdvanceAuthentication once and are calling it again using the MFA Answer Text Box submit button.
{
resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, MFAChallenge_DropDownList.SelectedItem.Value, MFAAnswer_Textbox.Text, true);
}
else //This is the first time calling AdvanceAuthenticaiton and we need to start polling and turn on our TimerTick while also displaying the MFA Answer Text Box for user input.
{
resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, MFAChallenge_DropDownList.SelectedItem.Value);
MFAAnswer_Label.Text = UiDrivenLogin.MechToPrompt(mech);
MFAAnswer_Label.Visible = true;
MFAAnswer_Textbox.Visible = true;
StartTextOob_Timer.Enabled = true; //Used to poll again every tick while waiting for user input in the form.
}
}
else
{
//User input is required for standard text mechanisms
if (mech["AnswerType"] == "Text")
{
resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, MFAChallenge_DropDownList.SelectedItem.Value, MFAAnswer_Textbox.Text);
}
//No user input required for polling mechanisms
else
{
resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, MFAChallenge_DropDownList.SelectedItem.Value);
MFALoad_Label.Text = ""; //Change the loading text back to default
}
MFAAnswer_Label.Visible = false;
MFAAnswer_Textbox.Visible = false;
}
}
//Check results
if (resultsDictionary["success"])
{
MFAChallenge_DropDownList.Visible = false;
MFAChallenge_DropDownList_Label.Visible = false;
//MFA was successful. We need to process the results to determine what should happen next.
if (resultsDictionary["Result"]["Summary"] == "StartNextChallenge" || resultsDictionary["Result"]["Summary"] == "LoginSuccess" || resultsDictionary["Result"]["Summary"] == "NewPackage")
{
StartTextOob_Timer.Enabled = false; //Turn off timer so that we are not polling in the background for StartTextOob
ProcessResults(resultsDictionary);
}
//If the summary is OobPending this means that StartTextOob has returned pending and we need to stop and wait. If any other results are present there is an error.
else if (resultsDictionary["Result"]["Summary"] != "OobPending")
{
//There was an error
Login_Div.Visible = false;
FailureText.Text = "There was an unexpected error. Please contact your system administrator. Click here to <a href=\"Login.aspx\">start over.</a>";
ErrorMessage.Visible = true;
}
}
else
{
//There was an error
Login_Div.Visible = false;
FailureText.Text = resultsDictionary["Message"] + " Click here to <a href=\"Login.aspx\">start over.</a>";
ErrorMessage.Visible = true;
}
}
UiDrivenLogin.AdvanceForMech()
also looks to see if the mechanism indicates StartTextOob
(which the code must poll until the authentication result is returned):
protected void AdvanceAuthentication(object sender, EventArgs e)
{
.
.
.
}
else
{
//If the mechanism is StartTextOob we need to allow the user to answer via text and poll at the same time.
if (mech["AnswerType"] == "StartTextOob")
{
if (StartTextOob_Timer.Enabled) //We have already called AdvanceAuthentication once and are calling it again using the MFA Answer Text Box submit button.
{
resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, MFAChallenge_DropDownList.SelectedItem.Value, MFAAnswer_Textbox.Text, true);
}
else //This is the first time calling AdvanceAuthenticaiton and we need to start polling and turn on our TimerTick while also displaying the MFA Answer Text Box for user input.
{
resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, MFAChallenge_DropDownList.SelectedItem.Value);
MFAAnswer_Label.Text = UiDrivenLogin.MechToPrompt(mech);
MFAAnswer_Label.Visible = true;
MFAAnswer_Textbox.Visible = true;
StartTextOob_Timer.Enabled = true; //Used to poll again every tick while waiting for user input in the form.
}
}
.
.
If an OOB challenge is selected, the code starts a timer which repeatedly checks for a Summary
value of StartNextChallenge
, LoginSuccess
, or NewPackage
:
protected void StartTextOob_TimerTick(object sender, EventArgs e)
{
//Start polling to see if user completed MFA remotely
RestClient authenticationClient = (RestClient)Session["AuthenticaitonClient"];
Dictionary<string, dynamic> resultsDictionary = UiDrivenLogin.AdvanceForMech(authenticationClient, authenticationClient.TenantId, authenticationClient.SessionId, RememberMe.Checked, null, MFAChallenge_DropDownList.SelectedItem.Value, "PollOnce", true);
//MFA was successful. We need to process the results to determine what should happen next.
if (resultsDictionary["Result"]["Summary"] == "StartNextChallenge" || resultsDictionary["Result"]["Summary"] == "LoginSuccess" || resultsDictionary["Result"]["Summary"] == "NewPackage")
{
StartTextOob_Timer.Enabled = false;
ProcessResults(resultsDictionary);
}
//If there is an error we need to stop polling
else if (!resultsDictionary["success"])
{
StartTextOob_Timer.Enabled = false;
Login_Div.Visible = false;
FailureText.Text = resultsDictionary["Message"] + " Click here to <a href=\"Login.aspx\">start over.</a>";
ErrorMessage.Visible = true;
}
}
If any of those three values are present, the code invokes ProcessResults()
, passing the dictionary of results obtained from invoking UiDrivenLogin.AdvanceForMech()
:
protected void ProcessResults(Dictionary<string, dynamic> resultsDictionary)
{
RestClient authenticationClient = (RestClient)Session["AuthenticaitonClient"];
//Once one challenge list has been completed, start the next one.
if (resultsDictionary["Result"]["Summary"] == "StartNextChallenge")
{
ProcessChallenges(authenticationClient.ChallengeCollection, true);
}
//If a new package was presented, start advance auth over again.
else if (resultsDictionary["Result"]["Summary"] == "NewPackage")
{
authenticationClient.ChallengeCollection = resultsDictionary["Result"]["Challenges"];
Session["AuthenticaitonClient"] = authenticationClient;
ProcessChallenges(resultsDictionary["Result"]["Challenges"]);
}
//Successful Login
else if (resultsDictionary["Result"]["Summary"] == "LoginSuccess")
{
//Login Complete
Login_Div.Visible = false;
Session["OTP"] = resultsDictionary["Result"]["Auth"];
//Make sure our fully authenticated client is updated in the session.
Session["AuthenticaitonClient"] = authenticationClient;
Session["isLoggedIn"] = "true";
UserManagement userManagementClient = new UserManagement(authenticationClient);
//Check if user is admin
Dictionary<string, dynamic> getUserInfo = userManagementClient.GetUserInfo();
if (getUserInfo["Result"]["IsSysAdmin"])
{
Session["isAdmin"] = "true";
}
if (Request.QueryString["redirect"] != null)
{
Server.Transfer(Request.QueryString["redirect"], true);
}
else
{
Server.Transfer("Default.aspx", true);
}
}
}
ProcessResults()
first obtains the RestClient
object that was stored by StartAuthentication()
. It then analyzes the Summary
element in the results dictionary. If the value is StartNextChallenge
or NewPackage
, then the code invokes ProcessChallenges()
to advance the authentication. If the value for Summary
is LoginSuccess
, the user has been authenticated. The code then redirects the user to a screen to perform other functionality offered by the client.
Updated about 5 years ago