Gal Ratner
Gal Ratner is a Techie who lives and works in Los Angeles. Follow galratner on Twitter Google
Deep dive into the LinkedIn API: Use C# to search for your dream Job (writing C# :))

LinkedIn has an extensive API designed to access almost every aspect of the site. Developers can access People and Connections, Groups, Companies, Jobs, Social Stream, Communications and more. There are two types of LinkedIn APIs: JavaScript based and REST based with the ability to connect the two. For example: you can log in with a JavaScript button and monitor your social updates using server based code. You can then send yourself summary emails with the updates.
This is a deep dive into both APIs. We are going to be using the JavaScript login button, showing the user’s name using HTML.  LinkedIn’s REST API uses OAuth authentication which we will utilize in order to get a new REST token. We will then re log in on the server and invoke the REST API, perform a jobs keyword search, return the results as JASON and show them to the user.

Before we start we will need a developer account. We can then log into the development portal and create a new application. We will name the application and get our API Key and Secrete Key. We will use them in order to authenticate.

Let’s begin by using the JavaScript API in order to create a login with LinkedIn button. The client code looks like this:

<script type="text/javascript" src="http://platform.linkedin.com/in.js">
        api_key: <%: Utils.APIKey %>
        authorize: true
        credentials_cookie: true
    </script>

 

<script type="in/Login">


 
api_key will be the application key and credentials_cookie will be set to indicate the API we wish to receive an authentication cookie. The cookie will contain the data we need in order to communicate with the REST API

.


 
We need to remember when running this code locally that a client to server request will only be accepted over https so we need to run IIS Express and use the local SSL certificate


 
 
Once the user have logged on we can display their details:

 

Hello, <?js= firstName ?> <?js= lastName ?>


We can now utilize the entire JavaScript API to make any call we wish. The full API is described here: https://developer.linkedin.com/javascript


Lets exchange the client calls for server calls


The first step will be to read the authentication cookie. The cookie contains all the values we need in order to exchange out JS token with a REST token.
{     "signature_method":"HMAC-SHA1",     "signature_order": ["access_token", "member_id"],     "access_token":"AD2dpVe1tOclAsNYsCri4nOatfstw7ZnMzWP",     "signature":"73f948524c6d1c07b5c554f6fc62d824eac68fee",     "member_id":"vvUNSej47H"     "signature_version": 1}

Let’s load it into a dynamic object

 

/// <summary>
        /// Get the cookie from the JSAPI
        /// </summary>
        /// <returns></returns>
        public static dynamic GetOauthCookie()
        {
            var oaut = HttpContext.Current.Request.Cookies[string.Format(oautCookie, APIKey)];
            dynamic cookie = null;
            if (oaut != null)
            {
                JavaScriptSerializer jss = new JavaScriptSerializer();
                cookie = jss.Deserialize<dynamic>(HttpUtility.UrlDecode(oaut.Value));
            }
            if (IsCookieValid(cookie))
                return cookie;
            return null;
        }

 


We must now make sure the cookie is valid and has not been tampered with in any way. We can do that by concatenating a string from the cookie values in the field names in the signature_order. Once we have them we can verify the hash generated from the values using our keys matches the hash in the cookie’s signature field.

 

/// <summary>
        /// Validate the cookie from the JSAPI
        /// </summary>
        /// <param name="cookie"></param>
        /// <returns></returns>
        public static bool IsCookieValid(dynamic cookie)
        {
            // The signature base is calculated by concatenating the values of the fields listed in the signature_order field in the order they appear.
            string signature = string.Empty;
            foreach (var fieldName in cookie["signature_order"])
                signature += cookie[fieldName];
            // After you construct the signature base, use the encryption algorithm specified in the signature_method to calculate the signature using your API secret.
            if (cookie["signature_method"] == "HMAC-SHA1")
            {
                return VerifyHash(signature, cookie["signature"]);
            }
            return true;
        }
 
        public static bool VerifyHash(string signature, string cookieSignature)
        {
            if (GetHMACSHA1Hash(SecretKey, signature) == cookieSignature)
                return true;
            return false;
        }
 
        public static string GetHMACSHA1Hash(string key, string value)
        {
            string hash = string.Empty;
            using (HMACSHA1 hmac = new HMACSHA1(System.Text.ASCIIEncoding.ASCII.GetBytes(key)))
            {
                hmac.ComputeHash(System.Text.ASCIIEncoding.ASCII.GetBytes(value));
                hash = System.Convert.ToBase64String(hmac.Hash);
            }
            return hash;
        }

 


If the values match this cookie is valid and we can continue with the REST token request.


The next part gets a little complicated so, let’s break it down to steps:
The cookie contains an access token. The access token needs to be posted into the LinkedIn server; however, since LinkedIn use OAuth for any REST request we need to make sure to follow the process to sign a request according to the OAuth 1.0a specs. You can read them here:  http://oauth.net/core/1.0a/.

Luckily we can find many libraries that can help is with OAuth. One of them is the open source class OAuthBase.cs that was written by Eran Sandler and can be found here: http://oauth.googlecode.com/svn/code/csharp/OAuthBase.cs .The example solution at the bottom of this page contains this class and you can feel free to investigate it further.


We need to post the access token as an OAuth  parameter so we are going to append xoauth_oauth2_access_token to the request.
The last step before we do the post is to append the generated OAuth signature.


Now we can make the request:

 

/// <summary>
        /// Exchange JSAPI Token for a REST API OAuth Token
        /// </summary>
        private void LoadRESTToken()
        {
            dynamic cookie = Utils.GetOauthCookie();
            // xoauth_oauth2_access_token parameter, set to the value of the access_token field in the cookie.
            string accessToken = cookie["access_token"];
            string url = String.Format("{0}{1}?xoauth_oauth2_access_token={2}"Utils.LinkedinAPI, Utils.AccessTokenPath, accessToken);
            OAuthBase oAuth = new OAuthBase();
            string nonce = oAuth.GenerateNonce();
            string timeStamp = oAuth.GenerateTimeStamp();
            string normalizedUrl = string.Empty;
            string normalizedRequestParameters = string.Empty;
            string signature = oAuth.GenerateSignature(new Uri(url), Utils.APIKey, Utils.SecretKey, accessToken, null"POST", timeStamp, nonce, out normalizedUrl, out normalizedRequestParameters);
            normalizedRequestParameters = normalizedRequestParameters += String.Format("&oauth_signature={0}"HttpUtility.UrlEncode(signature));
            string oAuthToken = Utils.GetOAuthToken(normalizedUrl, normalizedRequestParameters);
            //Load both token and secret into the current session
            context.Session["oauth_token"] = HttpUtility.ParseQueryString(oAuthToken)["oauth_token"];
            context.Session["oauth_token_secret"] = HttpUtility.ParseQueryString(oAuthToken)["oauth_token_secret"];
        }

 

 

/// <summary>
        /// Gets the REST API OAuth Token
        /// </summary>
        /// <param name="url"></param>
        /// <param name="postData"></param>
        /// <returns></returns>
        public static string GetOAuthToken(string url, string postData)
        {
            string token = string.Empty;
            using (WebClient client = new WebClient())
            {
                client.Headers.Add("content-type""application/x-www-form-urlencoded");
                try
                {
                    byte[] byteArray = Encoding.ASCII.GetBytes(postData);
                    byte[] responseArray = client.UploadData(url, "POST", byteArray);
                    token = Encoding.ASCII.GetString(responseArray);
                }
                catch (Exception ex)
                {
                    token = ex.Message;
                }
            }
 
            return token;
        }

 


The result is a string containing both the token and token secret. We now have access to the REST API!


With access to the rest API lets start building the UI and wiring the application. Lets build a text box for keyword searches and a JSON call in order to populate a list with the results:

 

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LinkedInJobFinder.Default" %>
<%@ Import Namespace="LinkedInJobFinder" %>
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script type="text/javascript" src="http://platform.linkedin.com/in.js">
        api_key: <%: Utils.APIKey %>
        authorize: true
        credentials_cookie: true
    </script>
    <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.3/jquery.min.js"></script>
    <script type="text/javascript">
    function searchListings(){
        jQuery.getJSON("JobData.ashx?jsoncallback=?",
        {
            Query: $("#searchText").val()
        },
        function (data) {
            $("#listingResults").empty();
            if (data.numResults == 0) {
                $("#listingResults").html("Sorry there are results. Please try again.");
                return;
            }
            $.each(data.jobs.values, function (i, item) {
                $("<b>Position</b><br>").appendTo("#listingResults");
                $("<p>Company: " + item.company.name + " location: " + item.locationDescription + "</p>").appendTo("#listingResults");
                $("<p>" + item.descriptionSnippet + "</p>").appendTo("#listingResults");
            });
        });
    }
</script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <script type="in/Login">
Hello, <?js= firstName ?> <?js= lastName ?>.<br />
Search for your dream job! <input type="text" name="searchText" id="searchText" /><br />
    <a href="javascript:searchListings();">Search</a>
</script><br /><br />
<div id="listingResults">
    </div>
 
    </form>
   
</body>
</html>

 


For improved performance, The JSON calls will be redirected to an HttpAsyncHandler and we are going to use an object pool from the Parallel Extensions Extras to allow for fast network calls.


Here is the handler’s code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
using System.Threading.Tasks;
using System.Text;
using System.Web.SessionState;
 
using OAuth;
 
namespace LinkedInJobFinder
{
    /// <summary>
    /// Summary description for JobData
    /// </summary>
    public class JobData : IHttpAsyncHandlerIRequiresSessionState
    {
        public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, Object extraData)
        {
            AsynchOperation asynch = new AsynchOperation(cb, context, extraData);
            asynch.StartAsyncWork();
            return asynch;
        }
 
        public void EndProcessRequest(IAsyncResult result)
        {
 
        }
 
        public void ProcessRequest(HttpContext context)
        {
            throw new InvalidOperationException();
        }
 
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
 
    class AsynchOperation : IAsyncResult
    {
        private bool completed;
        private Object state;
        private AsyncCallback callback;
        private HttpContext context;
 
        bool IAsyncResult.IsCompleted
        {
            get
            {
                return completed;
            }
        }
 
        WaitHandle IAsyncResult.AsyncWaitHandle
        {
            get
            {
                return null;
            }
        }
 
        Object IAsyncResult.AsyncState
        {
            get
            {
                return state;
            }
        }
 
        bool IAsyncResult.CompletedSynchronously
        {
            get
            {
                return false;
            }
        }
 
        public AsynchOperation(AsyncCallback callback, HttpContext context, Object state)
        {
            this.callback = callback;
            this.context = context;
            this.state = state;
            this.completed = false;
        }
 
        public void StartAsyncWork()
        {
            dynamic cookie = Utils.GetOauthCookie();
            // If this user has no session open it by getting a new OAuth 1.0a token for the REST API
            if (context.Session["oauth_token"] == null)
               LoadRESTToken();
 
            Task.Factory.StartNew(() =>
            {
                var jobRequest = Utils.JobRequestPool.GetObject();
 
                try
                {
                    string jobListings;
                    string keywordSearch = String.Format("{0}?keywords={1}"Utils.LinkedinJobAPIURL, HttpUtility.UrlEncode(context.Request.QueryString["Query"]));
                    jobListings = jobRequest.GetJobs(keywordSearch, Convert.ToString(context.Session["oauth_token"]), Convert.ToString(context.Session["oauth_token_secret"]));
                    context.Response.Write(context.Request.QueryString["jsoncallback"] + "(" + jobListings + ")");
                }
                catch (Exception)
                {
 
                }
                Utils.JobRequestPool.PutObject(jobRequest);
                completed = true;
                callback(this);
            });
        }
 
        /// <summary>
        /// Exchange JSAPI Token for a REST API OAuth Token
        /// </summary>
        private void LoadRESTToken()
        {
            dynamic cookie = Utils.GetOauthCookie();
            // xoauth_oauth2_access_token parameter, set to the value of the access_token field in the cookie.
            string accessToken = cookie["access_token"];
            string url = String.Format("{0}{1}?xoauth_oauth2_access_token={2}"Utils.LinkedinAPI, Utils.AccessTokenPath, accessToken);
            OAuthBase oAuth = new OAuthBase();
            string nonce = oAuth.GenerateNonce();
            string timeStamp = oAuth.GenerateTimeStamp();
            string normalizedUrl = string.Empty;
            string normalizedRequestParameters = string.Empty;
            string signature = oAuth.GenerateSignature(new Uri(url), Utils.APIKey, Utils.SecretKey, accessToken, null"POST", timeStamp, nonce, out normalizedUrl, out normalizedRequestParameters);
            normalizedRequestParameters = normalizedRequestParameters += String.Format("&oauth_signature={0}"HttpUtility.UrlEncode(signature));
            string oAuthToken = Utils.GetOAuthToken(normalizedUrl, normalizedRequestParameters);
            //Load both token and secret into the current session
            context.Session["oauth_token"] = HttpUtility.ParseQueryString(oAuthToken)["oauth_token"];
            context.Session["oauth_token_secret"] = HttpUtility.ParseQueryString(oAuthToken)["oauth_token_secret"];
        }
    }
}


The last step would be to actually make a keyword reach call to the LinkedIn REST endpoint and return the JSON result to the client. We need to remember to sign every request:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Net;
using System.IO;
using System.Xml.Linq;
using System.Text;
 
using OAuth;
 
namespace LinkedInJobFinder
{
    public class JobRequest : IDisposable
    {
        WebClient client;
        OAuthBase oAuth;
        private bool disposed = false;
 
        public JobRequest()
        {
            client = new WebClient();
            client.Headers.Add("x-li-format""json");
            client.UseDefaultCredentials = true;
            oAuth = new OAuthBase();
        }
 
        /// <summary>
        /// Download a json string of jobs using a keyword search
        /// </summary>
        /// <param name="searchFilter"></param>
        /// <param name="oauthToken"></param>
        /// <param name="oauthTokenSecret"></param>
        /// <returns></returns>
        public string GetJobs(string searchFilter, string oauthToken, string oauthTokenSecret)
        {
            string nonce = oAuth.GenerateNonce();
            string timeStamp = oAuth.GenerateTimeStamp();
            string normalizedUrl = string.Empty;
            string normalizedRequestParameters = string.Empty;
            string signature = oAuth.GenerateSignature(new Uri(searchFilter), Utils.APIKey, Utils.SecretKey, oauthToken, oauthTokenSecret, "GET", timeStamp, nonce, out normalizedUrl, out normalizedRequestParameters);
            normalizedRequestParameters = normalizedRequestParameters += String.Format("&oauth_signature={0}"HttpUtility.UrlEncode(signature));
 
            try
            {
                return client.DownloadString(String.Format("{0}?{1}", normalizedUrl, normalizedRequestParameters));
            }
            catch (Exception ex)
            {  
                return ex.Message;
            }
        }
 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (!disposed)
            {
                if (disposing)
                    client.Dispose();
                disposed = true;
            }
        }
    }
}


The results are a list of jobs available to the user:


 
Conclusion


This was a complete C# usage example of the LinkedIn API. You can find the API reference in the developer portal and download the sample code below.


Posted 15 Oct 2011 3:02 AM by Gal Ratner
Filed under: , ,

Powered by Community Server (Non-Commercial Edition), by Telligent Systems