# Custom

## Sample SSO using a .NET custom HTTP 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.

{% hint style="info" %}
This sample illustrates a very basic scenario that uses a separate IIS website as the remote authentication service provider to host a `login.aspx` page (e.g. to simulate an authentication service provider that validates user credentials against an identity provider) and WorkflowGen in another IIS website where the custom HTTP module is installed and configured.
{% endhint %}

{% hint style="info" %}
We recommend securing the WorkflowGen website with SSL and using encryption to secure the token. The code provided below is a basic sample, so you might have to customize it to enforce security and hide detailed error messages.
{% endhint %}

### WorkflowGen custom HTTP module authentication configuration

1. Download the [`CustomAuthModuleSSO 2.0`](https://advantys.box.com/s/ufkqlb5zp7d9vytyeugegw9u6vu6v0r5) 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`
   * All of your custom web forms' `\bin` folders<br>
2. 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.<br>
3. Edit your `\wfgen\web.config` file and add the following configuration:

   ```html
   <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>
   ```

   \
   ✏️ **Note:** Change the `{remoteauthserver:port}` value of the **RemoteAuthenticationServiceProviderLoginUrl** key to your existing remote authentication server name and port number.<br>
4. 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**.

### Remote Authentication Service Provider web server configuration

1. 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.<br>
2. Edit `login.aspx` and change the `{workflowgenserver:port}` to your WorkflowGen server name and port number.<br>
3. In IIS, change the **Authentication** configuration. Enable `Anonymous Authentication` on the remote authentication server IIS website.<br>
4. Open the `http://{remoteauthserver:port}/login.aspx` URL in a browser and make sure it's accessible and working properly.

### How the sample works

1. 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.

   ```html
   http://{remoteauthserver:port}/login.aspx?return_url=http%3A%2F%2F%7Bworkflowgenserver%3Aport%7D%2Fwfgen
   ```
2. Your remote authentication server will show a login page for the user to input their username and password for submission.<br>
3. 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.

   ```url
   http://{workflowgenserver:port}/wfgen?token=dXNlcm5hbWU%3D
   ```
4. 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.<br>
5. 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.

   ```markup
   http://{workflowgenserver:port}/wfgen/logout

   http://{workflowgenserver:port}/wfgen?logout=true

   http://{workflowgenserver:port}/wfgen?logout=y
   ```

{% hint style="info" %}
For a more secure token, the remote authentication server (`login.aspx` in this sample) can generate a [JSON Web Token](https://jwt.io) containing the user information and sign it using a shared secret key. WorkflowGen must know the shared secret key in order to verify and retrieve the user information from the JWT. There are many JWT signing and verification libraries available on [jwt.io](https://jwt.io).
{% endhint %}

### `CustomAuthModuleSSO.cs` source code&#x20;

```csharp
//*********************************************************************
// 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));
        }
    } 
}
```

### `login.aspx` source code

```html
<%@ 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>
```

## Generic sample code for an HTTP module

### Overview

This 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 [Custom HTTP Basic authentication](https://docs.advantys.com/docs/tech/security#custom-http-basic-authentication) in the [WorkflowGen Technical Guide](https://docs.advantys.com/docs/tech).

{% hint style="info" %}
This sample uses the common **Basic authentication** method to request and retrieve the user credentials. It can be easily modified to use any other **standard** or **custom** methods, such as a `token` or a `username` stored in a cookie, a query string parameter, a form data parameter, or a server variable.\
\
The **main** **objective** of this custom HTTP module is to create and set the **GenericPrincipal** object with a valid login username (in clear text) of the current HTTP request context that will be later used by WorkflowGen to verify and load the user's profile.
{% endhint %}

### Source code

```csharp
//*************************************************************************
// Copyright © 2025
//
// 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));
        }
    }
}
```
