The majority of WorkflowGen clients use Windows Integrated Authentication in IIS, although several other modes are supported. By using Integrated Authentication, users are seamlessly authenticated to WorkflowGen by using the Windows local session (network password) of the user.
WorkflowGen also supports .NET web form-based authentication and custom HTTP module authentication for single sign-in (SSO) integration.
If your organization has implemented the Shibboleth ecosystem, you can integrate WorkflowGen with the Shibboleth SP3 SSO module.
The advantage of the custom HTTP module solution is that it secures all WorkflowGen HTTP requests, including web services. It also provides more customization possibilities than the form authentication-based solution.
This section focuses on the custom HTTP module authentication solution.
Download the sample package, unzip the package, then copy the \VS Project\bin\Release\CustomAuthModuleSSO.dll file into the following folders:
\wfgen\bin
\wfgen\wfapps\webforms\bin
Copy the \Remote auth service provider\login.aspx sample file from the package to your remote authentication server IIS website's root folder. If there's no existing website, then create a new IIS website on your web server and copy the file to the root folder. Normally, the remote authentication server IIS website is a separate website from the WorkflowGen IIS website.
✏️ Note: Don't forget to update the {remoteauthserver:port} value for the RemoteAuthenticationServiceProviderLoginUrl key in the \wfgen\web.config file if the remote authentication server name and port are different or have changed. See step 3 of the previous section.
Edit login.aspx
When a user connects to WorkflowGen (http://{workflowgenserver:port}/wfgen), CustomAuthModuleSSO checks if a token is available (in the Cookies, QueryString, Form, or Server variables collections) in order to retrieve the current login user. If no token is found, then it will redirect the user to your remote authentication server login.aspx page with a return URL.
CustomAuthModuleSSO.cs source code login.aspx source codeThis sample is an HTTP module that uses the HTTP_AUTHORIZATION server variable for authentication. You must insert your own method to authenticate users.
For more information, see in the
All of your custom web forms' \bin folders
In IIS, change the Authentication configuration. Enable Anonymous Authentication on all of the following IIS applications under the WorkflowGen website:
\wfgen
\wfgen\wfapps\webforms
All of your custom web form folders
✏️ Note: All of your web service apps (subfolders in the \wfgen\wfapps\webservices folder) should be using Basic authentication, since this sample redirects to the remote authentication server's login.aspx page, which is not supported in the web service scenario.
Edit your \wfgen\web.config file and add the following configuration:
✏️ Note: Change the {remoteauthserver:port} value of the RemoteAuthenticationServiceProviderLoginUrl key to your existing remote authentication server name and port number.
If you want to customize the behavior of CustomAuthModuleSSO, open the \VS Project\CustomAuthModuleSSO.sln solution in Visual Studio, then edit the CustomAuthModuleSSO.cs file. When ready, rebuild the solution and redeploy the \VS Project\bin\Release\CustomAuthModuleSSO.dll file as in step 1.
{workflowgenserver:port}In IIS, change the Authentication configuration. Enable Anonymous Authentication on the remote authentication server IIS website.
Open the http://{remoteauthserver:port}/login.aspx URL in a browser and make sure it's accessible and working properly.
When the user submits the form, the login page validates the user credentials against the authentication service provider (code to implement in login.aspx). If the credentials are valid, then the page generates a token and sets it as a parameter in the WorkflowGen return URL. For the simplicity of this sample, the token only contains the username that is encoded in base64.
When WorkflowGen receives the new HTTP request from your remote authentication server, CustomAuthModuleSSO will retrieve the token from the QueryString. It decodes the token to retrieve the username and creates a GenericPrincipal object used to set the current user session, then it saves the token as a cookie for future HTTP requests. WorkflowGen will now use the user principal (GenericPrincipal object) of the HTTP request context to verify and load the WorkflowGen user's profile.
If the user is invalid (e.g. no matching username is found in the database), WorkflowGen will reject the user and display a Security error: You are not authorized to view this page error message.
For sign out, the user can use one of the following URLs. WorkflowGen will clear the token cookie, which will force the user to log in if they want to access WorkflowGen again.
<configuration>
<appSettings>
<add key="RemoteAuthenticationServiceProviderLoginUrl" value="http://{remoteauthserver:port}/login.aspx" />
</appSettings>
<system.webServer>
<modules>
<add name="ApplicationSecurityAuthenticationModule" type="WorkflowGen.Samples.CustomAuthModule" />
</modules>
</system.webServer>
</configuration>http://{workflowgenserver:port}/wfgen?token=dXNlcm5hbWU%3Dhttp://{workflowgenserver:port}/wfgen/logout
http://{workflowgenserver:port}/wfgen?logout=true
http://{workflowgenserver:port}/wfgen?logout=yhttp://{remoteauthserver:port}/login.aspx?return_url=http%3A%2F%2F%7Bworkflowgenserver%3Aport%7D%2Fwfgen//*********************************************************************
// Sample code for an HTTP Authentication module
//*********************************************************************
using System;
using System.Web;
using System.Security.Principal;
using System.Text;
using System.Configuration;
namespace WorkflowGen.Samples
{
/// <summary>
/// Summary description for CustomAuthModule
/// </summary>
// ReSharper disable once UnusedMember.Global
public class CustomAuthModule : IHttpModule
{
private static readonly string RemoteAuthenticationServiceProviderLoginUrl =
ConfigurationManager.AppSettings["RemoteAuthenticationServiceProviderLoginUrl"];
private const string TokenName = "token";
private const bool OnAuthenticationErrorRedirect = true;
/// <summary>
/// Release any resources
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Initialization of module
/// </summary>
public void Init(HttpApplication application)
{
application.AuthenticateRequest += Application_AuthenticateRequest;
}
/// <summary>
/// Delegate for authenticating a request
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Application_AuthenticateRequest(object sender, EventArgs e)
{
var application = sender as HttpApplication;
Authenticate(application);
}
/// <summary>
/// Authenticate a user
/// </summary>
/// <param name="application"></param>
private void Authenticate(HttpApplication application)
{
var request = application.Context.Request;
var response = application.Context.Response;
var server = application.Context.Server;
// if sign out request from the remote authentication service provider
if (request.RawUrl.EndsWith("/logout") || request["logout"] != null &&
(request["logout"].ToLower() == "true" || request["logout"].ToLower() == "y"))
{
// Clear the token cookie when user logout
var tokenCookie = new HttpCookie(TokenName) {Expires = DateTime.UtcNow.AddDays(-1d)};
application.Response.Cookies.Add(tokenCookie);
response.Redirect(RemoteAuthenticationServiceProviderLoginUrl);
}
string token = null;
// Retrieve the authentication token from a QueryString parameter
if (request[TokenName] != null)
{
token = request[TokenName];
}
string returnUrl = server.UrlEncode(request.Url.Query == string.Empty
? request.Url.AbsoluteUri
: request.Url.AbsoluteUri.Replace(request.Url.Query, string.Empty));
// If the token is empty
if (string.IsNullOrEmpty(token))
{
// Redirect to back to the Remote Authentication Service Provider login page
response.Redirect(RemoteAuthenticationServiceProviderLoginUrl + "?return_url=" + returnUrl);
return;
}
string username = null;
try
{
username = Base64Decode(token);
}
catch (Exception e)
{
if (OnAuthenticationErrorRedirect)
{
// Redirect to back to the Remote Authentication Service Provider login page
response.Redirect(RemoteAuthenticationServiceProviderLoginUrl + "?return_url=" + returnUrl);
return;
}
else
{
throw new Exception($"Unable to decode the token: {e.Message}");
}
}
try
{
// Create and set this user for the session
application.Context.User =
new GenericPrincipal(new GenericIdentity(username, "Basic"), new[] {"user"});
// Store the token in cookie for the current user session if not exists
if (request.Cookies[TokenName] == null)
{
var tokenCookie = new HttpCookie(TokenName, Base64Encode(username))
{
Expires = DateTime.UtcNow.AddMinutes(20)
};
application.Response.Cookies.Add(tokenCookie);
}
}
catch (Exception e)
{
if (OnAuthenticationErrorRedirect)
{
// Redirect to back to the Remote Authentication Service Provider login page
response.Redirect(RemoteAuthenticationServiceProviderLoginUrl + "?return_url=" + returnUrl);
}
else
{
throw new Exception($"WorkflowGen authentication error: {e.Message}");
}
}
}
private static string Base64Encode(string message)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(message));
}
private static string Base64Decode(string message)
{
return Encoding.UTF8.GetString(Convert.FromBase64String(message));
}
}
}<%@ Import Namespace="System.Web.Security" %>
<html>
<head>
<title>Remote Authentication Service Provider</title>
<script runat="server" language="C#">
void btnSubmit_Click(Object Source, EventArgs e)
{
// Perform custom username and password validation here.
// ...
// If all OK then redirect back to WorkflowGen with the token in QueryString of the URL.
// For simplicity, the token contains the username encoded in base64.
Response.Redirect((!string.IsNullOrEmpty(Request["return_url"]) ? Request["return_url"] : "http://{workflowgenserver:port}/wfgen") +
"?token=" + HttpContext.Current.Server.UrlEncode(Base64Encode(txtUsername.Text)));
}
string Base64Encode(string message)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(message));
}
</script>
</head>
<body
class="AuthenticationPage"
onload="document.getElementById('txtUsername').focus();"
>
<div class="Div">
<form method="Post" runat="server">
<fieldset class="FieldSet">
<legend class="Legend">Remote Authentication Service Provider</legend>
<table class="Table" cellpadding="0" cellspacing="0">
<tr class="Row">
<td class="CellCaption">
<span class="Username">Username:</span>
</td>
<td class="CellValue">
<asp:TextBox
CssClass="InputUsername"
ID="txtUsername"
runat="server"
/>
</td>
</tr>
<tr class="Row">
<td class="CellCaption">
<span class="Password">Password:</span>
</td>
<td class="CellValue">
<asp:TextBox
CssClass="InputUsername"
ID="txtPassword"
runat="server"
/>
</td>
</tr>
</table>
<table class="Footer" cellpadding="0" cellspacing="0">
<tr class="Row">
<td class="Cell">
<asp:Button
CssClass="Button"
ID="btnSubmit"
Text="Submit"
OnClick="btnSubmit_Click"
runat="server"
/>
</td>
</tr>
</table>
</fieldset>
</form>
</div>
</body>
</html>//*************************************************************************
// Copyright © 2021
//
// Purpose: Generic sample code for an HTTP module using HTTP_AUTHORIZATION
//
//*************************************************************************
using System;
using System.Web;
using System.Security.Principal;
using System.Text;
using System.Diagnostics;
namespace MyCompany.Hosting.Samples
{
/// <summary>
/// Summary description for CustomAuthModule
/// </summary>
public class CustomAuthModule : IHttpModule
{
/// <summary>
/// Constructor
/// </summary>
public CustomAuthModule()
{
}
/// <summary>
/// Release any resources
/// </summary>
public void Dispose()
{
}
/// <summary>
/// Initialization of module
/// </summary>
/// <param name="context"></param>
public void Init(HttpApplication application)
{
application.AuthenticateRequest += new EventHandler(application_AuthenticateRequest);
}
/// <summary>
/// Delegate for authenticating a request
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void application_AuthenticateRequest(object sender, EventArgs e)
{
HttpApplication application = sender as HttpApplication;
Debug.Assert(application != null);
// Has the client sent Authorization information
if (application.Context.Request.ServerVariables["HTTP_AUTHORIZATION"] == null)
{
// Redirects client to send HTTP Basic credentials
application.Response.StatusCode = 401;
application.Response.AddHeader("WWW-Authenticate", "Basic");
application.Response.End();
}
else
{
// Retrieve authentication string
string authString = application.Request.ServerVariables["HTTP_AUTHORIZATION"];
// If the authentication method is basic
if (authString.StartsWith("basic", StringComparison.InvariantCultureIgnoreCase))
{
string[] credentials;
bool isValidUser = false;
// Decode credentials
credentials = Base64Decode(authString.Substring(6)).Split(':');
// Credentials should have two cells
// credentials[0] is the username
// credentials[1] is the password
Debug.Assert(credentials.Length == 2);
// ****************************
// Perform your check here
// ****************************
// isValidUser = YourAuthenticationMethod(credentials[0], credentials[1]);
if (isValidUser)
{
// Create a user
GenericPrincipal user =
new GenericPrincipal(
new GenericIdentity(credentials[0], authString),
new string[] { "role_name" });
// Set this user for the session
application.Context.User = user;
}
}
}
}
private static string Base64Decode(string message)
{
return Encoding.UTF8.GetString(Convert.FromBase64String(message));
}
}
}Clients that are already using Shibboleth Service Provider 3 to manage resource access (such as services or applications) can quickly integrate with WorkflowGen by offering users Single Sign-On (SSO) authentication for a secure and seamless user experience.
For this, Shibboleth SP3 provides a native module for IIS 7 and later that passes user information to WorkflowGen via the REMOTE_USER environment variable.
The WorkflowGen website and web apps should have only the Anonymous Authentication method enabled under the IIS Manager Authentication feature page.
For installation and configuration details, see the documentation.