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); } }
NOTE: IAuthenticationPlugin 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. 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. |
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. |
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
}
}
}
}
});
}
}