Out of the box Telligent Community comes with a selection of oAuth client providers. Your users can use LinkedIn, Facebook, Salesforce, Twitter, Google and Microsoft Live. There are lot's of other social networks out there and sometimes you need to create a provider for one of these. In this post I will detail how to create a oAuth provider for Strava, which is a popular cycling and running network. Those that know of my adventures will understand why I picked this particular site.
Starting Point
The starting point to create a new provider is to create a new class inherited from Telligent.Evolution.Extensibility.Authentication.Version1.IOAuthClient By implementing this interface we will be simply be able to enable this plugin to gain an additional provider.
public interface IOAuthClient : IPlugin
{
//The url that is populated by the system when the provider returns from the initial request
//this is then used to get the access token, Salesforce explains this in a nice
//diagram https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_understanding_web_server_oauth_flow.htm
string CallbackUrl { get; set; }
//Any javascript scripts that should be run when the user logs out of the community
string ClientLogoutScript { get; }
//The name of the oAuth client that is displayed to the user
string ClientName { get; }
//The internal name of the client stored in the database
string ClientType { get; }
//The special key that is needed to create an oauth token
string ConsumerKey { get; }
//The special secret that is needed to create an oauth token
string ConsumerSecret { get; }
//Enabled or disabled (this is not used)
bool Enabled { get; }
//Url to icon to be displayed to the end user
string IconUrl { get; }
//Privacy terms to be displayed to the end user
string Privacy { get; }
//A color that represents this oauth provider
string ThemeColor { get; }
//Called to get the URL of the far end server to start the authentication process
string GetAuthorizationLink();
//Called when a repsonse is recieved from the provider, returns oauth data that is stored by the platform
OAuthData ProcessLogin(HttpContextBase context);
}
This interface is actually pretty straightforwards. Let's start with the quick wins and then get onto the more technical ones.
ConsumerKey and ConsumerSecret are both configuration properties of the plugin, when you create your application on the oAuth server it will provide these values. Since these values are required lets also inherit our new plugin from IRequiredConfigurationPlugin.IRequiredConfigurationPlugin. This interface tells Telligent that is the plugin is not configured then don't enable it, achieved through the property IsConfigured. Notice that in the UI the string displayed to the end user is Client ID, this matches the Strava administration page, so the end user is not confused.
public class StravaOAuth2Client : IOAuthClient, IRequiredConfigurationPlugin
{
public string ConsumerKey
{
get
{
if (Configuration != null)
return Configuration.GetString("ConsumerKey");
return string.Empty;
}
}
public bool IsConfigured
{
get
{
if (!string.IsNullOrEmpty(ConsumerKey))
return !string.IsNullOrEmpty(ConsumerSecret);
return false;
}
}
public PropertyGroup[] ConfigurationOptions
{
get
{
var propertyGroupArray = new PropertyGroup[1]
{
new PropertyGroup("options", "Options", 0)
};
var property1 = new Property("ConsumerKey", "Client ID", PropertyType.String, 0, "");
property1.Rules.Add(new PropertyRule(typeof (TrimStringRule), false));
propertyGroupArray[0].Properties.Add(property1);
var property2 = new Property("ConsumerSecret", "Consumer Secret", PropertyType.String, 0, "");
property2.Rules.Add(new PropertyRule(typeof (TrimStringRule), false));
propertyGroupArray[0].Properties.Add(property2);
return propertyGroupArray;
}
}
public void Update(IPluginConfiguration configuration)
{
Configuration = configuration;
}
protected IPluginConfiguration Configuration { get; private set; }
}
Other quick wins include the client name, the color of the theme and plugin details.
public string ClientType
{
get { return "strava"; }
}
public string Name
{
get { return "4 Roads - Strava OAuth Client"; }
}
public string Description
{
get { return "Provides user authentication through Strava"; }
}
public string ThemeColor
{
get { return "FC4C02"; }
}
public string ClientName
{
get { return _translatablePluginController.GetLanguageResourceValue("OAuth_Strava_Name"); }
}
public string Privacy
{
get { return _translatablePluginController.GetLanguageResourceValue("OAuth_Strava_Privacy"); }
}
Notice here I have user the ITranslatablePluginController through the member variable _translatablePluginController. This is provided using the Servant pattern when you implement the ITranslatablePlugin interface. This interface extends your plugin to support resource strings.
public void SetController(ITranslatablePluginController controller)
{
_translatablePluginController = controller;
}
public Translation[] DefaultTranslations
{
get
{
Translation en = new Translation("en-us");
en.Set("OAuth_Strava_Name", "Strava");
en.Set("OAuth_Strava_Privacy", "By signing in with Strava, data from your profile, such as your name, userID, and email address, will be collected so that an account can be created for you. Your Facebook password will not be collected. Please click on the link at the bottom of the page to be directed to our privacy policy for information on how the collected data will be protected.");
return new[] { en };
}
}
The IconURL property needs to return a URL to an icon for Strava. To do this we can include this icon in the project assembly and use 4 Roads helper class CfsFilestoreInstaller. Telligent stores it's oauth images in the CFS folder oauthimages, this is considered a system folder so we will install our images in a similar folder "customoauthimages" then write the code to return the URL using that. You will notice in the final code that in the FileInstaller plugin that we have also implemented the ICentralizedFileStore to tell the platform to create a filestore area.
public string IconUrl { get { try { ICentralizedFile file = CentralizedFileStorage.GetFileStore(FileInstaller.FILESTOREKEY).GetFile(string.Empty, "strava.png"); if (file != null) return file.GetDownloadUrl(); return null; } catch { return null; } } } }
To make a request to Strava, Telligent needs to know where to post the request to. The method GetAuthorizationLink gives this information to Telligent. Notice in our code that we have made the base URL into a configuration parameter so that if Strava ever changes it's URL it would not require a code change in the plugin.
public string GetAuthorizationLink() { return string.Format("{0}?client_id={1}&response_type=code&scope=view_private&redirect_uri={2}", AuthorizeBaseUrl, ConsumerKey, HttpUtility.UrlEncode(CallbackUrl)); }
To complete the provider we need to implement the other methods, the one that requires a little explanation is the ProcessLogin method. We have created a helper set of functions to allow us to implement this part of the code easily.
Using the helper functions we can process the incoming request from Strava and then if the data is valid, create a OAuthData object that contains the standard user information. To make coding easier note the use of dynamic objects when using JSON responses.
public OAuthData ProcessLogin(HttpContextBase context) { if (!Enabled || context.Request.QueryString["error"] != null || OAuthFunctions.GetVerificationParameters(context) == null) AuthenticationFailed(); CallbackUrl = OAuthFunctions.RemoveVerificationCodeFromUri(context); dynamic parameters = GetAccessTokenResponse(OAuthFunctions.GetVerificationParameters(context)); if (string.IsNullOrEmpty((string)parameters.access_token)) AuthenticationFailed(); return ParseUserProfileInformation(parameters); }
The final call in the above method creates a OAuthData object that represents the user the platform will either link this account to an existing user or create a new account.
private OAuthData ParseUserProfileInformation(dynamic repsonseJObject) { dynamic athlete = repsonseJObject.athlete; string clientUserId = athlete.id; var authData = new OAuthData(); authData.ClientId = clientUserId; authData.ClientType = ClientType; authData.Email = athlete.email; authData.UserName = GetUniquueUserName(athlete); //Note although this is called every time the user authenticates it's only used when the account is first created authData.AvatarUrl = athlete.profile; authData.CommonName = athlete.firstname + athlete.lastname; return authData; }
And that just about covers the whole process, the working provider can be downloaded from github here in the Nexus2 project and if you would like any further information please contact me on this site or use our website http://www.4-roads.com
Thank you to Ben Tiedt and Patrick M. for working with us to ensure that everything was correct.