Code Sample: SharePoint-to-LinkedIn Connector

Applies to: SharePoint Server 2010

In this article
Introduction to LinkedIn Integration
Structure of the Solution
OAuth Authentication and LinkedIn
Using the LinkedIn API
Creating the Timer Job
SharePoint User Profile Storage
Managing Tokens
Creating the Opt-in Page
Updating the LinkedIn Timer Job
Updating User Status
Building and Running the Sample

This sample demonstrates how to add social data from other social networking sites to your MySite, how to create and use new user profile properties, and how to implement new timer jobs that are managed on your SharePoint Central Administration site. The first part of the solution requires the user to grant the SharePoint application permission to act on behalf of the user. This "opt in" process is handled by the OAuth Authentication standard that is used by LinkedIn and many other social computing sites. The signup page handles the acquisition and storage of the token that is necessary to allow the application to update the LinkedIn data. The second part of the solution is implemented as a timer job that queries for user profile changes. The timer job requires a management page, to enable and disable the timer job.

Sample code provided by:MVP Contributor Mathew McDermott, Catapult Systems | MVP Contributor Andrew Connell, Critical Path Training, LLC

Install this code sample on your own computer by downloading the Microsoft SharePoint 2010 Software Development Kit (SDK) or by downloading the sample from Code Gallery. If you download the SharePoint 2010 SDK, the sample is installed in the following location on your file system: C:\Program Files\Microsoft SDKs\SharePoint 2010\Samples\Social Data and User Profiles.

Introduction to LinkedIn Integration

As businesses embrace social computing throughout the enterprise, it might be necessary to publish information from inside the organization to external providers of social computing platforms. As these providers are reaching out to broader audiences, many are implementing open APIs that provide the ability to create applications that access the user’s information.

This solution updates the LinkedIn status for a Microsoft SharePoint Server 2010 user when the user includes the text #li at the end of a status note. A big challenge facing application development from any platform is secure authentication from the custom application to the service provider. LinkedIn uses the OAuth standard for authentication and API access. This requires the developer to apply for API access and receive an API key pair to use in the application. After that process is complete, the solution can be deployed and users can sign up to use the application.

Structure of the Solution

The first part of the solution requires the user to grant the SharePoint application permission to act on behalf of the user. This "opt n" process is handled by the OAuth Authentication standard. For more information about the LinkedIn authentication process, see the LinkedIn Developer API documentation. This solution implements a signup page for the individual users to opt in to our application. The signup page (shown in Figure 1) needs to handle the acquisition and storage of the token that is necessary to allow the application to update the LinkedIn data.

Figure 1. LinkedIn opt-in page

LinkedIn opt-in page

The second part of the solution is a timer job that queries for user profile changes. If the LinkedIn Status Note field changes, and includes the text #li, the solution sends that update to the user’s LinkedIn account. The timer job requires a management page (shown in Figure 2) to enable and disable the timer job.

Figure 2. Manage the LinkedIn Timer Job page

Manage the LinkedIn Timer Job page

OAuth Authentication and LinkedIn

The details of the OAuth standard are available on the OAuth Standards site. An open source .NET library, DotNetOpenAuth, handles the interactions that are required to implement the Oauth transactions The LinkedIn Toolkit, a CodePlex project that uses the DotNetOpenAuth library, handles the LinkedIn API details for this solution.

Using the LinkedIn API

To begin development with the LinkedIn API you must apply for an API key and follow these basic steps.

To get started with the LinkedIn API

  1. Request an API key from the LinkedIn Developer Portal.

  2. Use the key in your application, to enable users to register your application.

  3. After the users sign up, store the user token to enable the application to perform the status updates on behalf of the users.

Creating the Timer Job

For more information about the steps required to create a timer job, see Creating Custom Timer Jobs in Windows SharePoint Services 3.0. This solution changes some of the implementation details from that article, to perform the LinkedIn status updates.

SharePoint User Profile Storage

The initial challenge for this solution is to perform the authentication from SharePoint to the external system and to store the authentication tokens without requiring the user’s password. User profiles simplify the task of storing each user’s authentication token. The fields shown in Table 1 must be configured by the Farm Administrator or by the FeatureActivated event receiver. The token field should not be visible to the user.

Table 1. Fields that must be configured by the Farm Administrator or by the FeatureActivated event receiver

Name

Display Name

Purpose

Type

LI-Token

LinkedIn Secure Token

Store the authentication tokens that are required by the application.

String

LI-Status

Update LinkedIn Status

Store the user’s preference for updating LinkedIn Status.

Boolean

Managing Tokens

The LinkedIn Toolkit and the DotNetOpenAuth library require the developer to create a token manager that handles the storage and retrieval of the OAuth tokens. This token processing is handled by the custom class that implements the IConsumerTokenManager interface. The IConsumerTokenManager interface is called by the DotNetOpenAuth library to handle the storage and retrieval of the tokens that are needed for user authentication in the OAuth standard.

This solution’s SPTokenManager class stores the tokens in user profile properties.

The constructor takes the UserProfile object as a parameter.

public SPTokenManager(UserProfile userProfile, string consumerKey, string consumerSecret)
{      
  if (String.IsNullOrEmpty(consumerKey))
  {
    throw new ArgumentNullException("consumerKey");
  }
    this.userProfile = userProfile;
    this.ConsumerKey = consumerKey;
    this.ConsumerSecret = consumerSecret;
}

The ExpireRequestTokenAndStoreNewAccessToken method clears the previously stored request token and stores a new access token.

public void ExpireRequestTokenAndStoreNewAccessToken(string consumerKey, string requestToken, string accessToken, string accessTokenSecret)
{
//Clear the previous request token and its secret, and store the new access token and its secret.  
Debug.WriteLine(String.Format("[OAuth] ExpireRequestTokenAndStoreNewRequestToken : {0} {1}", accessToken, accessTokenSecret));
SetUserProfileValue(requestToken, "", TokenType.RequestToken);
SetUserProfileValue(accessToken, accessTokenSecret, TokenType.AccessToken);
}

The StoreNewRequestToken method accepts the authentication request and response and extracts and stores the necessary tokens.

public void StoreNewRequestToken(UnauthorizedTokenRequest request, ITokenSecretContainingMessage response)
{
//Store the "request token" and "token secret" for later use
Debug.WriteLine(String.Format("[OAuth] StoreNewRequestToken : {0} {1}", response.Token, response.TokenSecret));
SetUserProfileValue(response.Token, response.TokenSecret, TokenType.RequestToken);
}

The GetTokenSecret method returns the token secret that corresponds to the requested token (either the Request token or the Access token).

public string GetTokenSecret(string token)
{
  //Return the token secret for the request token OR access token that 
  //you are given.   
  return GetUserProfileValue(token);
}

These methods call one of two utility functions that read and write the user profile values in a format that can be retrieved later by the timer job. The SetUserProfile method takes the parameters of the token value, the token secret, and the token type. The user profile field is updated based on the token type, and the values are stored as a four-item array of string objects. When the user is initially signing up for the service, the updates are performed as a GET request. The application accounts for this by initially setting the AllowUnsafeUpdates attribute of the current Web to true for the duration of the transaction.

private void SetUserProfileValue(string token, string tokenSecret, TokenType type)
{
  Debug.WriteLine(String.Format("[OAuth] Set the User Profile Value for {0} {1} ", token, tokenSecret));
  UserProfile profile = GetUserProfile();
  bool allowUnsafeUpdates = SPContext.Current.Web.AllowUnsafeUpdates;
  //The tokens are stored as an array of String.
  try
  {
    SPContext.Current.Web.AllowUnsafeUpdates = true;
    //Does out Profile Field exist?
    if (profile[Globals.MSDNLI_TokenField] != null)
    {
      string[] delim = { "|" };
      string[] strTokenArr = new string[4];
      //Does the field contain values?
      if (profile[Globals.MSDNLI_TokenField].Value != null)
      {
        //Get the values.
        strTokenArr = profile[Globals.MSDNLI_TokenField].Value.ToString().Split(delim, StringSplitOptions.None);
      }
      switch (type)
      {
        case TokenType.AccessToken:
          strTokenArr[0] = token;
          strTokenArr[1] = tokenSecret;
          break;
        case TokenType.InvalidToken:
          break;
        case TokenType.RequestToken:
          strTokenArr[2] = token;
          strTokenArr[3] = tokenSecret;
          break;
        default:
          break;
      }
      profile[Globals.MSDNLI_TokenField].Value = String.Format("{0}|{1}|{2}|{3}", strTokenArr);
      profile.Commit();
    }
  }
  catch (Exception ex)
  {
    Debug.WriteLine(String.Format("Failed to load the User Profile. The error was: {0}", ex.Message));
  }
  finally
  {
    SPContext.Current.Web.AllowUnsafeUpdates = allowUnsafeUpdates;
  }
}

The GetUserProfile method takes a token value and returns the associated token secret from storage.

private string GetUserProfileValue(string token)
{
  Debug.WriteLine("[OAuth] Get the User Profile Value for " + token);
  UserProfile profile = GetUserProfile();
  //Check the LinkedIn properties.
  try
  {
    if (profile[Globals.MSDNLI_TokenField] != null)
    {
      string[] delim = { "|" };
      string[] strTokenArr = new string[4];
      strTokenArr = profile[Globals.MSDNLI_TokenField].Value.ToString().Split(delim, StringSplitOptions.None);
      //Get the values.
      return strTokenArr[Array.IndexOf(strTokenArr, token)+1]; 
    }
    else
    {
      return null;
    }
  }
  catch (Exception ex)
  {
    Debug.WriteLine(String.Format("Failed to load the User Profile. The error was: {0}", ex.Message));
    return null;
  }
}

Creating the Opt-in Page

When the user visits the opt-in page, the UserProfile object that represents the current user is inspected, and that user’s LinkedIn authentication status is determined. If the appropriate token is found, the associated LinkedIn profile is loaded. If the token is not found, the Opt In button is enabled. The code for the page handles this interaction by implementing properties for the TokenManager object, the AccessToken object, and the Authorization object that are required to create the transactions for LinkedIn. The TokenManager pulls the global ApiKeys acquired from LinkedIn and supplies them to your custom application.

The TokenManager object is created from the consumer key values and the custom UserProfile property values.

private SPTokenManager TokenManager
{
  get
  {
return new SPTokenManager(GetUserProfile(), consumerKey, consumerSecret);
   }

When the page loads, the application begins by testing the state of the Authorization object. It creates a new Authorization object by using the TokenManager object and the AccessToken objects

    //Begin by testing the Authorization state for the user.
    this.Authorization = new WebOAuthAuthorization(this.TokenManager, this.AccessToken);

If this is a return trip from a visit to LinkedIn to initiate authentication, the page needs to complete the authorization process.

if (!IsPostBack)
{
  //Do we need to complete the Authorization rocess?
  string accessToken = this.Authorization.CompleteAuthorize();
                
  if (accessToken != null)
  {
    //If the AccessToken is not null, store it.
    this.AccessToken = accessToken;
    Debug.WriteLine("[OAuth] Redirect: " + Request.Path);
    //Get the user back to where they belong.
    Response.Redirect(Request.Path);
   }
  }

After the application completes the process, it can take the Access token and attempt to retrieve the LinkedIn profile for the current user.

//Finally, if ready, get the LinkedIn profile.
if (this.AccessToken != null)
{
  try
  {
    LoadLinkedInProfile();
  }
  catch (Exception ex)
  {
    Debug.WriteLine("[MSDN] Error loading LinkedIn Profile: " + ex.Message);
  }
 }

If the user has never used this feature before, the Opt In button is enabled. When clicked, it calls the BeginAuthorize method. The LinkedIn toolkit takes the API keys and passes them to LinkedIn. LinkedIn returns a page where the user can enter authentication credentials. All of this is handled by the LinkedIn API.

protected void btnOptIn_Click(object sender, EventArgs e)
{
  //Initiate the authorization process.
  this.Authorization.BeginAuthorize();           
}

Updating the LinkedIn Timer Job

The custom LinkedIn connection timer job uses the SPTokenManager object to read and authorize the status updates for the user. At a regular interval, the timer job queries for UserProfile objects whose Status Note fields have been updated. The timer job builds a list of UserProfile objects and passes them to the method that is responsible for performing the update. In the RetrieveUserProfileChanges method, the following code determines whether the Status Note contains the #li string and adds the profile to the list of profiles whose status has been updated.

//If the property has the token in it, add it to the list.
if (statusNote.Trim().EndsWith(Globals.MSDNLI_StatusTag))
{
  Debug.WriteLine("[MSDN] We found the change token in: " + statusNote);
  changedUsers.Add(propertyChange.AccountName, propertyChange.ChangedProfile);
}

Finally, all the user profiles are enumerated and the updates are sent to LinkedIn one at a time by the UpdateLinkedIn method.

private void UpdateLinkedIn(UserProfile profile)
{
  //Init AccessToken to null; we are going to get it from the profile.
  string AccessToken = null;

  //Use the UserProfile to fetch the LinkedIn tokens.
  try
  {
    if ((profile[Globals.MSDNLI_TokenField] != null) && (profile[Globals.MSDNLI_TokenField].Value != null))
    {
      string[] delim = {"|"};
      string[] strTokenArr = profile[Globals.MSDNLI_TokenField].Value.ToString().Split(delim, StringSplitOptions.None);
      //Get the values.
      if ((strTokenArr != null) || (strTokenArr.Length == 4))
      {
        //Retrieve the access token.
        AccessToken = strTokenArr[0];
        Debug.WriteLine(String.Format("[MSDN] Retrieved the LinkedIn token for user: {0}", profile.DisplayName));
       }
       else
       {
         throw new Exception(String.Format("[MSDN] LinkedIn update failed for user {0}. Profile token field is not formatted correctly.", profile.DisplayName));
        }
      }
    }
    catch (Exception ex)
    {
      Debug.WriteLine("[MSDN] " + ex.Message);
    }

    if ((AccessToken != null) && (AccessToken != String.Empty))
    {
      try
      {
        //Create a token manager.
        SPTokenManager TokenManager = new SPTokenManager(profile, Globals.liApiKey, Globals.liSecretKey);
        //Prep the Authorization state for the user.
        WebOAuthAuthorization Authorization = new WebOAuthAuthorization(TokenManager, AccessToken);
        //Get an instance of the service.
        LinkedInService service = new LinkedInService(Authorization);
        //Issue the update.
        string statusMessage = profile[Globals.MSDNLI_ProfileField].Value.ToString();
        Debug.WriteLine(String.Format("[MSDN] Sending status update to LinkedIn: {0}", statusMessage ));
        service.UpdateStatus(statusMessage);
                    
       }
       catch (LinkedInException li)
       {
         Debug.WriteLine(String.Format("[MSDN] LinkedIn threw an error: {0}", li.Message));
       }
       catch (Exception ex)
       {
         Debug.WriteLine(String.Format("[MSDN] Error updating LinkedIn for user {0} the error was: {1}", profile.DisplayName, ex.Message));
       }
                
     }
   }

Updating User Status

The user experience for posting a status update to LinkedIn is simple. The user adds the text #li to the end of the SharePoint status text box. After the timer job executes, the update is sent to LinkedIn.

Building and Running the Sample

The following steps demonstrate how you can test this project on your development or test site.

To build the sample

  1. Create a folder named Microsoft.SDK.Server.Samples, and then unzip the MSDN LinkedIn Code.zip file in it.

  2. Add the LinkedIn.dll file and the DotNetOpenAuth.dll file (located in MSDN LinkedIn Code\Shared Libraries\) to the global assembly cache. For guidance about how to do this in your development or testing environment, see How to: Install an Assembly into the Global Assembly Cache.

  3. Start Visual Studio 2010, and then open the LinkedInConnection.sln file that is in the folder that you created in step 1.

  4. In the Properties window, specify the site URL value of the absolute address of your development or test site (for example, http://mysite/. Ensure that you include the closing forward slash. Also, configure the using statement (using (SPSite site = new SPSite("servername"))) in the LinkedInConnection.EventReceiver.cs file so that it uses this same URL value.

  5. In the SendColleagueURLNote.EventReceiver.cs file, specify the URL for your SharePoint Central Administration site in the SendColleagueURLNoteEventReceiver method.

  6. If they are not already present, add references to the following assemblies to the project:

    • Microsoft.SharePoint.dll

    • Microsoft.SharePoint.ApplicationPages.Administration

    • Microsoft.SharePoint.Security

    • Microsoft.Office.Server.dll

    • Microsoft.Office.Server.UserProfiles.dll

    • DotNetOpenAuth.dll

    • LinkedIn.dll

  7. Get an API key from LinkedIn.

  8. Configure the LinkedIn OAuth Redirect URL back to your page in SharePoint. For example, http://mysite/_layouts/msdn/lisettings.aspx.

  9. In the Elements\menuItem.xml file, configure the Url property of the UrlAction element to point to that same SharePoint page (http://mysite/_layouts/msdn/lisettings.aspx).

  10. In the Globals.cs file, configure the MSDNLI_ConsumerKey value and the MSDNLI_SecretKey value with your keys.

  11. On the Build menu, select Deploy Solution. After the build is complete, the application page is installed on your development or test site.

To run the sample

  1. After the solution is built and deployed, go to your Central Administration site to set the LinkedIn timer job (http://myCentralAdminSite/_admin/msdn/LinkedInTimer.aspx).

  2. Go to http://mysite/_layouts/msdn/lisettings.aspx, and then click Opt In to sign in to your LinkedIn account and opt in to the connector.

See Also

Tasks

How to: Create and Edit a User Profile Property

Other Resources

Creating Custom Timer Jobs in Windows SharePoint Services 3.0

LinkedIn Developer API documentation

LinkedIn API Keys

OAuth Standards site

DotNetOpenAuth

LinkedIn Toolkit