Article Single Sign-On Plugins

Custom Single Sign-On Providers

Single Sign-On providers are plugins that facilitate authentication using an identity provider.  This means user information and credentials are stored in a third party application and the community will delegate the responsibility of identifying and authenticating users to this service.    Your community comes with two options for single sign-on, Cookies based authentication and OpenId Connect.  You may however need to integrate your own provider for various reasons.  You do this by creating a plugin the implements Telligent.Evolution.Extensibility.Security.Version3.IAuthenticationPlugin.

namespace Telligent.Evolution.Extensibility.Security.Version3
{
    public interface IAuthenticationPlugin : ISingletonPlugin
    {
        void Configure(AuthenticationConfigurationController controller);
        Task Authenticate(AuthenticationOptions options);
    }
}


NOTEIAuthenticationPlugin is a singleton plugin type, meaning only one can be enabled at any given time.

Async Ready

The interface methods and function delegates in this plugin type all return Task.   This means they all can be declared async making it easier to use with other async centered classes/methods such as HttpClient.   If you do not use any async methods, you can wrap your async methods in TaskUtility.FromSync as the example here does.

Configuring the Provider

The Configure() method defines specific behavior that should occur during the login process.  To do this it utilizes a series of Func<options, Task> that are executed at the appropriate times.  

Configuring Login

AuthenticationConfigurationController.Login is a delegate function that is called once a user or the platform initiates the login process and is most often used to set up any external redirects, such as to an authorization server.  To work with this function you are given a LoginOptions object to manipulate.

LoginOptions

HttpRequest

Read

An HttpRequestBase object that represents the current login request. 

ExternalLoginUrl

Read/Write

By setting this value you are telling the community it should redirect to this URL for authentication and not the community login page.   When not set the standard login screen is displayed and the SSO pipeline is terminated.

LoginCallbackUrl

Read

This is where the identity provider must redirect the request originating from the ExternalLoginUrl when authentication is complete.  It MUST at minimum also  supply an email address and username, or be able to derive a username from the email address. 

This Url should be transmitted as part of ExternalLoginUrl, usually by query string parameter.   For example if this was using OAuth, the this would be set in the redirect_uri parameter.

Note that any query string data passed to this callback URL will be available when authenticating a user.

CommunityReturnUrl

Read

This is where a user should be redirected to once authentication is complete.   At this stage it is best passed to the ExternalLoginUrl in some way such as a state parameter so it can be re-sent back to the LoginCallbackUrl

Here is an example of a login method building an appropriate URL.  Note since we are not doing any async work here we use TaskUtility.FromSync() to execute a synchronous stack.  First it takes the callback URL and appends the community return URL as a query string parameter with the expectation that it will be included when the identity provider redirects to this callback URL.

public void Configure(AuthenticationConfigurationController controller)
	{
		controller.Login = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var callback = options.LoginCallbackUrl + $"?returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
					options.ExternalLoginUrl = $"https://identityprovider.com/login?callback={Uri.EscapeDataString(callback)}";
				});
			};
		
	};
}


Configuring Logout

Logout is very similar to login except in this scenario you asking for the session at the identity provider to be terminated after the community session via HTTP redirect.


LogoutOptions

HttpRequest

Read

An HttpRequestBase object that represents the current logout request. 

ExternalLogoutUrl

Read/Write

By setting this value you are telling the community it should redirect to this URL to end the session at the identity provider after terminating the community session.

When not provided the user is logged out of the community only and no additional steps are taken.

LogoutCallbackUrl

Read

This is where the identity provider must redirect the request originating from the ExternalLogoutUrl when session termination is complete. 

This should transmitted as part of ExternalLogoutUrl, usually by query string parameter.   The LogoutCallbackUrl will have additional query string data that should be preserved on the redirect.  Specifically it will contain a state parameter that is a Base64 Url encoded string of data, including the return Url.

Here is an example where we add the logout logic to redirect a user to our identity provider to end their session.  We pass the entire callback to this endpoint with the expectation we will be redirected back to this URL with all additional query string data.  Notice we don't add a community return URL here as if one exists it will already be included as part of the URL.

public void Configure(AuthenticationConfigurationController controller)
	{
		controller.Login = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var callback = options.LoginCallbackUrl + $"?returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
					options.ExternalLoginUrl = $"https://identityprovider.com/login?callback={Uri.EscapeDataString(callback)}";
				});
			};
			controller.Logout = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var callback = options.LogoutCallbackUrl;
					options.ExternalLogoutUrl = $"https://identityprovider.com/logout?callback={Uri.EscapeDataString(callback)}";
				});
			};
		
	};
}

Configuring Change Password

AuthenticationConfigurationController.ChangePassword  is a delegate function that is called once a user decides to change their password or reset it.  For external identity providers this generally will redirect the user to a page on the identity provider to change the password or reset it.


ChangePasswordOptions

HttpRequest

Read

An HttpRequestBase object that represents the current login request. 

PasswordRequestType

Read

An enum value that indicates whether this was a change password request(ChangePassword) or a forgot password(ForgotPassword) request if you need to differentiate.

ChangePasswordUrl

Read/Write

By setting this value you are telling the community it should redirect to this Url for changing a password on an external identity provider.  Not setting this will result in the default behavior for the community.

CommunityReturnUrl

Read

This is where a user should be redirected to once the password operation is complete.   At this stage it is best passed to the ChangePasswordUrl in some way such as a state parameter so it can be used to redirect back to the community.

In this example the change password is provided to redirect the user to change their password.   The community return URL, if available is appended as a query string parameter so that the identity provider can redirect the user to that URL.   

public void Configure(AuthenticationConfigurationController controller)
	{
		controller.Login = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var callback = options.LoginCallbackUrl + $"?returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
					options.ExternalLoginUrl = $"identityprovider.com/login?callback={Uri.EscapeDataString(callback)}";
				});
			};
			controller.Logout = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var callback = options.LogoutCallbackUrl;
					options.ExternalLogoutUrl = $"https://identityprovider.com/logout?callback={Uri.EscapeDataString(callback)}";
				});
			};
			controller.ChangePassword = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var mode = options.PasswordRequestType.ToString().ToLowerInvariant();	
					options.ChangePasswordUrl = $"https://identityprovider.com/password?mode={mode}&returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
				});
			};
		
	};
}


Configuring Registration

AuthenticationConfigurationController.RegisterUser  is a delegate function that is called when a request to register for the site is made  For external identity providers this generally will redirect the user to a page on the identity provider to create an account.


RegisterUserOptions

HttpRequest

Read

An HttpRequestBase object that represents the current login request. 

RegisterUserUrl

Read/Write

By setting this value you are telling the community it should redirect to this Url to register new user accounts.  Not setting this will result in the default behavior for the community.

CommunityReturnUrl

Read

This is where a user should be redirected to once registration operation is complete.   At this stage it is best passed to the RegisterUserUrl in some way such as a state parameter so it can be used to redirect back to the community.

In this example the register user URL on the identity provider is provided to redirect the user to create an account.   The community return URL, if available is appended as a query string parameter so that the identity provider can redirect the user to that URL. 

 

public void Configure(AuthenticationConfigurationController controller)
	{
		controller.Login = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var callback = options.LoginCallbackUrl + $"?returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
					options.ExternalLoginUrl = $"identityprovider.com/login?callback={Uri.EscapeDataString(callback)}";
				});
			};
			controller.Logout = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var callback = options.LogoutCallbackUrl;
					options.ExternalLogoutUrl = $"https://identityprovider.com/logout?callback={Uri.EscapeDataString(callback)}";
				});
			};
			controller.RegisterUser = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					options.RegisterUserUrl = $"https://identityprovider.com/register?returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
				});
			};
			controller.ChangePassword = (options) =>
			{
				return TaskUtility.FromSync(() =>
				{
					var mode = options.PasswordRequestType.ToString().ToLowerInvariant();	
					options.ChangePasswordUrl = $"https://identityprovider.com/password?mode={mode}&returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
				});
			};
		
	};
}


Authenticating a User

The actual process of authentication occurs after the request has been redirected to the identity provider, the identity provider authenticates the user and then redirects the user to the LoginCallbackUrl with the appropriate data to authenticate a user.   In the example we are reading a simple cookie.  Note that in a production scenario this cookie would be protected/encrypted.

Note any HTTP context specific data passed to the callback is available in Authenticate.

AuthenticationOptions

HttpContext

Read

An HttpContextBase object that represents the current login request ad response. 

AuthenticatedUserId

Read/Write

Once a user is deciphered or created, setting this value to the user Id determines what user will be considered logged in.  Returning null indicates the authentication failed

CommunityReturnUrl

Read

Set this to redirect the user after authentication.  Generally this value was passed to the ExternalLoginUrl and back through to the callback.

SessionState

Read/Write

Values here are stored as part of the user’s community session and retrievable on the User API object at User.AuthenticatedSessionState.   NOTE:  Only the accessing user can view their own session state.

This is meant for simple data you may need to use later such as an external User Id or short access token.  NEVER store large amounts of data here.

LoginCallbackUrl

Read

The callback url invoked by the identity provider on the community.  Generally this is the Url invoking Authenticate.  It is provided for informational purposes, such as needing to pass it to a token endpoint and a redirect_uri.


Here we examine the cookie to see if we got a user, then attempt to retrieve the user by the email address(email addresses have a tendency to be more unique than usernames between two systems).  If we find the user, we set the AuthenticatedUserId to the user we found.   If we do not find a user, then you can simply set this to null which will bubble an authentication error to the front end, or if you have enough information, you can create the user.  If you do choose to auto-create the user, never use the email address as the username as that will disclose the email to users without the owner's consent.  Instead generate a username as you see fit.

Once we have established the user we set CommunityReturnUrl to the return URL value from the query string of the callback request, which if you recall we passed to login so we would have access to it here.

Full Example

public class SampleAuthenticationPlugin : Extensibility.Security.Version3.IAuthenticationPlugin
    {
		public string Name => "Sample SSO Plugin";

        public string Description => "A simple SSO Sample";

		public void Initialize()
		{
			
		}

		public void Configure(AuthenticationConfigurationController controller)
    	{
    		controller.Login = (options) =>
    			{
    				return TaskUtility.FromSync(() =>
    				{
    					var callback = options.LoginCallbackUrl + $"?returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
    					options.ExternalLoginUrl = $"identityprovider.com/login?callback={Uri.EscapeDataString(callback)}";
    				});
    			};
    			controller.Logout = (options) =>
    			{
    				return TaskUtility.FromSync(() =>
    				{
    					var callback = options.LogoutCallbackUrl;
    					options.ExternalLogoutUrl = $"https://identityprovider.com/logout?callback={Uri.EscapeDataString(callback)}";
    				});
    			};
    			controller.ChangePassword = (options) =>
    			{
    				return TaskUtility.FromSync(() =>
    				{
    					var mode = options.PasswordRequestType.ToString().ToLowerInvariant();	
    					options.ChangePasswordUrl = $"https://identityprovider.com/password?mode={mode}&returnUrl={Uri.EscapeDataString(options.CommunityReturnUrl)}";
    				});
    			};
    		
    	    };
        }
		public Task Authenticate(AuthenticationOptions options)
        {
			return TaskUtility.FromSync(() =>
			{
				var cookie = options.HttpContext.Request.Cookies["sso_user"];
				var returnUrl = options.HttpContext.Request.QueryString["returnUrl"];
				if (cookie != null)
                {
					var username = cookie.Values["username"];
					var email = cookie.Values["email"];	
					if(!string.IsNullOrEmpty(username) && ! string.IsNullOrEmpty(email))
                    {
						var user = Extensibility.Apis.Get<Extensibility.Api.Version1.IUsers>().Get(new Extensibility.Api.Version1.UsersGetOptions() { Email = email });
						if(!user.HasErrors())
                        {
							options.AuthenticatedUserId = user.Id.Value;
							if(!string.IsNullOrEmpty(returnUrl))
								options.CommunityReturnUrl =Uri.UnescapeDataString(returnUrl);	
                        }
                        else
                        {
							if(user.Errors.Any(e=>e.HttpStatusCode == 404))
                            {
								//Here you could create the user if not found, its up to you
                            }
                        }
                    }
                }
			});
		}

}