Using LinkedIn as an Identity Provider for SharePoint 2010

Updated 5/29/2013: LinkedIn has changed the format of the profile URLs it sends back. Consequently, I had to change the GetProfileId function in my STS code. The code in this post has been updated. Thanks to Dee for commenting and bringing the issue to my attention!

Updated 1/27/2013: I am pleased to report that the steps below also work for setting up LinkedIn as an identity provider for SharePoint 2013! The SharePoint screens will look a little different, but the steps to set up the integration are the same. If you run into any issues with this setup in SharePoint 2013, please leave a comment at the bottom of this post.

Updated 1/22/2013: After almost a year, I thought I would go back and revisit this post (as it continues to be one of the most popular posts on my blog). As it turns out, some updates to the code were required and many of the LinkedIn user interfaces required to complete this integration had changed. I can verify that the steps below work properly and that all screenshots are current as of January 22, 2013. Please leave a comment on this post if you run into any issues.

Those of you who have seen me speak at a user group or SharePoint Saturday event recently know how much I love Windows Azure AppFabric’s Access Control Services and how easy ACS makes it to configure SharePoint 2010 to allow users to log in with OpenID identity providers such as Facebook, Google, Yahoo!, and Windows Live. But what about LinkedIn? While LinkedIn isn’t an OpenID provider per se, numerous sites on the web allow users to sign in using their LinkedIn account credentials via OAuth. When the subject of external identity providers for a future version of the SharePoint User Group of Washington, DC site came up during my recent presentation there, the audience overwhelmingly agreed that LinkedIn was the most “professional” identity provider to integrate (and certainly one that most SharePoint professionals would feel more comfortable using than Facebook). There was only one problem…Azure AppFabric ACS does not natively support LinkedIn as an identity provider. What was I to do?

As one astute observer pointed out during my presentation, ACS is not required to configure SharePoint 2010 to interact with any external identity provider. While ACS greatly simplifies the management and configuration required to set up an external identity provider and its associated claim rules, it is possible to write code that leverages a custom STS (Security Token Service) to manage all of this without involving Azure at all.

Getting Started

Luckily for me, the vast majority of the technical “heavy lifting” required to accomplish this integration had already been done by Travis Nielsen. In this blog post, Travis details the steps (and development prerequisites, including the Windows Identity Foundation SDK 4.0) required to integrate SharePoint 2010 with Facebook as an identity provider using a custom STS (and without using Azure ACS). The steps for integrating LinkedIn are basically the same as they are for Facebook. Below I will detail how configuring the two identity providers differs.

C# OAuth Class for LinkedIn

I was preparing to have to adapt the Facebook OAuth class Travis had found to work with LinkedIn. Fortunately, I stumbled upon a C# OAuth class for LinkedIn that had already been developed by Fatih YASAR. All credit for this aspect of the solution belongs to him.

Creating the Application

Much like configuring Facebook as an identity provider requires the creation of an “application” within Facebook, LinkedIn requires the creation of an application as well.

Anyone with a LinkedIn account can create an application through the LinkedIn Developer Network.

1. Go to https://www.linkedin.com/secure/developer.

lidn

2. Click Add New Application. Fill out the form to register your new application. Any values you enter here can be changed later on.

3. Press Add Application. The following screen will confirm creation of your LinkedIn application.

newlinkedinapp

4. As with Facebook, it is important that you take note of the API Key and Secret Key values that are displayed. We will need to use these values in our code (note that you are also provided with OAuth User Token and OAuth User Secret values; these are not required for this setup). Press Done. We will return to our application setup when we have identified the SharePoint 2010 web application with which we want to integrate LinkedIn.

Customizing the STS

First, add the oAuthBase2 and oAuthLinkedIn classes from Fatih YASAR to the App_Code folder of your STS project. You shouldn’t need to make any changes to the two .cs files, but you will notice that the oAuthLinkedIn class expects to find your API Key and Secret Key values in the <appSettings> of your STS’s Web.config, so add them there:

apiKeys

While we are here, it’s worth noting that the STS is configured by default to use the certificate with CN=STSTestCert to sign the SAML tokens it generates containing the claims we configure it to send. This certificate is installed as part of the Windows Identity Framework SDK. We will need to export this certificate so that we can configure SharePoint 2010 to use our custom STS as a Trusted Identity Provider and add this certificate to its trusted certificate store. I was able to find and export this certificate by loading the Certificates (Local Computer) snap-in and navigating to Trusted People > Certificates:

ststestcert

It goes without saying that you would not want to use this certificate in a production environment. Remember to update the SigningCertificateName in your STS’s Web.config to match the name of your production certificate.

Back to the STS code…I tied everything together in Login.aspx.cs. When the login page first loads (with no oauth_token value present in the query string), the user is redirected to the appropriate authorization link (hosted by LinkedIn). Once the user has an access token from LinkedIn, we are able to populate a series of claims for that user. Much like Travis did with Facebook, I make a call to the LinkedIn API to get profile information associated with the current user and define a series of output claims based on this information. The claims I have defined are:

  • Name – concatenated first and last name
  • Webpage – the LinkedIn user’s profile URL
  • NameIdentifier – the LinkedIn user’s profile ID (parsed from the profile URL)
  • GivenName and Surname (just because I could)

Below is the code that I used. In a production environment, you will want to include better error handling and more robust XML parsing!

public partial class Login : System.Web.UI.Page
{
    private oAuthLinkedIn liAuth = new oAuthLinkedIn();
    private string profileId = string.Empty;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (Request.QueryString["oauth_token"] == null)
        {
            // If "oauth_token" does not appear in the query string, the user has not been authenticated
            // This call sets the Token and TokenSecret values
            string authLink = liAuth.AuthorizationLinkGet();
            Application["requestToken"] = liAuth.Token;
            Application["requestTokenSecret"] = liAuth.TokenSecret;
            Response.Redirect(authLink);
        }
        else
        {
            // User has been authenticated
            // Use the request token to get the access token
            liAuth.Token = (string)Application["requestToken"];
            liAuth.TokenSecret = (string)Application["requestTokenSecret"];
            liAuth.Verifier = Request.QueryString["oauth_verifier"];
            liAuth.AccessTokenGet(Request.QueryString["oauth_token"]);

            if (liAuth.Token.Length > 0)
            {
                // Make a LinkedIn API call to get claim information
                Dictionary<string, string> claims = GetClaims();
                HttpContext.Current.Session.Add("LinkedInClaims", claims);
                // Set FedAuth cookie (without this line, SharePoint will not consider the user logged in)
                FormsAuthentication.SetAuthCookie(profileId, false);
                // Pass along the query string - STS default.aspx will redirect to SharePoint
                Response.Redirect("default.aspx?" + Request.QueryString.ToString());
            }
        }
    }

    private Dictionary<string, string> GetClaims()
    {
        Dictionary<string, string> claims = new Dictionary<string, string>();
        string response = liAuth.APIWebRequest("GET", "https://api.linkedin.com/v1/people/~", null);

        if (!string.IsNullOrEmpty(response))
        {
            string firstName = string.Empty;
            string lastName = string.Empty;
            string headline = string.Empty;
            string url = string.Empty;

            // Parse values from response XML
            using (XmlReader reader = XmlReader.Create(new StringReader(response)))
            {
                reader.ReadToFollowing("first-name");
                firstName = reader.ReadElementContentAsString();
                reader.ReadToFollowing("last-name");
                lastName = reader.ReadElementContentAsString();
                reader.ReadToFollowing("headline");
                headline = reader.ReadElementContentAsString();
                reader.ReadToFollowing("url");
                url = reader.ReadElementContentAsString();
                profileId = GetProfileId(url);
            }

            claims.Add(System.IdentityModel.Claims.ClaimTypes.Name, string.Format("{0} {1}", firstName, lastName));
            claims.Add(System.IdentityModel.Claims.ClaimTypes.Webpage, url);
            claims.Add(System.IdentityModel.Claims.ClaimTypes.NameIdentifier, profileId);
            claims.Add(System.IdentityModel.Claims.ClaimTypes.GivenName, firstName);
            claims.Add(System.IdentityModel.Claims.ClaimTypes.Surname, lastName);
        }

        return claims;
    }

    private string GetProfileId(string url)
    {
        // Parse the profile ID from the profile URL
        string id = url.Substring(url.IndexOf("id=") + 3);
        return id.Substring(0, id.IndexOf('&'));
    }
}

Also, don’t forget to update the GetOutputClaimsIdentity() function in the CustomSecurityTokenService.cs class to make use of the output claims that are stored in a Session variable:

protected override IClaimsIdentity GetOutputClaimsIdentity( IClaimsPrincipal principal, RequestSecurityToken request, Scope scope )
{
    if ( null == principal )
    {
        throw new ArgumentNullException( "principal" );
    }

    ClaimsIdentity outputIdentity = new ClaimsIdentity();

    var oAuth20Claims = HttpContext.Current.Session["LinkedInClaims"] as Dictionary<string, string>;
    if (oAuth20Claims != null)
    {
        foreach (var claim in oAuth20Claims)
        {
            outputIdentity.Claims.Add(new Claim(claim.Key, claim.Value));
        }
    }

    return outputIdentity;
}

Deploying the Custom STS and Updating the OAuth Redirect URL for the LinkedIn Application

Following the steps outlined in Travis’ blog post will give you an ASP.NET 4.0 web site that serves as your Security Token Service. You must deploy or publish this web site so that your SharePoint users will have access to it. For the purposes of this blog post, I have set up an IIS web site at http://sts.contoso.com that points to my custom STS. (Remember to set the .NET Framework version of your STS application pool to v4.0!)

Once I have deployed my STS, I need to return to my LinkedIn application setup and provide the URL to this STS as the OAuth Redirection URL, with a couple of important query string parameters appended:

1. Return to https://www.linkedin.com/secure/developer and click the name of your application.

2. In the OAuth User Agreement section near the bottom of the form, enter the URL to the Default.aspx page at the root of your STS site with the following query string:
?wa=wsignin1.0&wtrealm=http%3a%2f%2fintranet.contoso.com%2f_trust%2f

oauthagree

These parameters are defined in greater detail here. The wtrealm parameter should be the URL-encoded value of your SharePoint web application with /_trust/ (%2f_trust%2f) appended. As you can see, you also have the option of specifying a different URL if the user selects “Cancel” from the authorization dialog. Finally, you may also optionally specify a URL to an 80×80 logo image for your app (that must use SSL).

3. Press Save to update your application.

Configuring the Trusted Identity Provider for SharePoint 2010

We will configure our LinkedIn Trusted Identity Provider for SharePoint 2010 to map the following claim types that are included in the SAML tokens we receive from our custom STS:

  • http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier (LinkedIn profile ID)
  • http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name (First and last name)
  • http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage (LinkedIn profile URL)

This is done by running the following PowerShell script:

$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2("c:\STSTestCert.cer")
$map1 = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier" -IncomingClaimTypeDisplayName "LinkedIn ID" -LocalClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn"
$map2 = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" -IncomingClaimTypeDisplayName "Display Name" -LocalClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
$map3 = New-SPClaimTypeMapping -IncomingClaimType "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage" -IncomingClaimTypeDisplayName "LinkedIn URL" -SameAsIncoming
$realm = "http://intranet.contoso.com/_trust"
$signinurl = "http://sts.contoso.com/"
New-SPTrustedIdentityTokenIssuer -Name "LinkedIn" -Description "LinkedIn custom STS" -Realm $realm -ImportTrustCertificate $cert -ClaimsMappings $map1,$map2,$map3 -SignInUrl $signinurl -IdentifierClaim $map1.InputClaimType
New-SPTrustedRootAuthority -Name "LinkedIn" -Certificate $cert

The script is fairly straightforward. A couple items to note:

  • The $cert defined in line 1 is created based on the exported STS signing certificate mentioned previously. In this case, I exported the certificate to my C:\ drive and reference it there.
  • Because certain claim type mappings are already used by SharePoint (such as name and nameidentifier), I had to define different LocalClaimType values for 2 of my 3 claim mappings.
  • I use the LinkedIn profile ID (and not the first and last name of the user) as the IdentifierClaim because only the profile ID is guaranteed to be unique (e.g., two different users could have the same first and last names). I will write some custom code to update the display name property of the SPUser objects associated with LinkedIn users so that they see their first and last name (instead of that 7-digit ID) at the top of the screen when they sign in to SharePoint.

Running this script will add a Trusted Identity Provider called LinkedIn to the list of Trusted Identity Providers that can be added to any web application through Central Administration:

After adding the new Trusted Identity Provider, it helps to define a User Permission policy for the web application that allows any users who authenticate using this Trusted Identity Provider to be authorized to have read access to the web application:

addusers

The Moment of Truth

Users should now be able to sign in to SharePoint with their LinkedIn accounts. Let’s give it a shot! Depending on the different authentication providers configured for a given web application, you may or may not see a sign-in page allowing you to choose which credentials to use to log in to SharePoint. We will choose LinkedIn:

A series of HTTP redirects will take place. If the user has previously logged in to LinkedIn and has a cookie, that user will not need to enter his/her credentials again. In this screen, the user is agreeing to allow the LinkedIn application to have access to his or her account information (which consists of name, profile headline, and profile URL):

signin2

If the user has not previously logged in to LinkedIn and/or does not have a cookie, the following screen will appear:

signin1

Users always retain the ability to revoke your application’s permission at any time. Press Allow access and some more HTTP redirects will take place that should eventually land the user back in SharePoint. I have used Travis Nielsen’s Claims Web Part on the team site where users log in with LinkedIn. Here you can see the claims mappings we configured earlier and how those claims are presented to SharePoint from our custom STS:

cvwp3

Because we are using the user’s LinkedIn profile ID as the nameidentifier claim, that value initially appears at the top right of the page when the user first signs in:

To improve the personalization experience for the end user, we can write the following code (in a web part, for instance) to update the DisplayName property of the SPUser to read the value from the givenname claim instead:

public class LinkedInNameChanger : WebPart
{
    string givenName = string.Empty;
    IClaimsIdentity claimsIdentity;
    Label lblOutput = new Label();
    Button btnFixUserName = new Button();

    protected override void OnLoad(EventArgs e)
    {
        // Read the incoming claims and assign values
        IClaimsPrincipal claimsPrincipal = Page.User as IClaimsPrincipal;
        claimsIdentity = (IClaimsIdentity)claimsPrincipal.Identity;

        foreach (Claim c in claimsIdentity.Claims)
        {
            if (c.ClaimType.EndsWith("givenname"))
            {
                givenName = c.Value;
                break;
            }
        }
    }

    protected override void CreateChildControls()
    {
        btnFixUserName.Text = "Fix Display Name";
        btnFixUserName.Click += new EventHandler(btnFixUserName_Click);
        this.Controls.Add(btnFixUserName);
        this.Controls.Add(lblOutput);
    }

    void btnFixUserName_Click(object sender, EventArgs e)
    {
        try
        {
            Guid siteId = SPContext.Current.Site.ID;
            Guid webId = SPContext.Current.Web.ID;
            SPUser currentUser = SPContext.Current.Web.CurrentUser;
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                using (SPSite site = new SPSite(siteId))
                {
                    using (SPWeb web = site.OpenWeb(webId))
                    {
                        web.AllowUnsafeUpdates = true;
                        SPUser user = web.AllUsers[currentUser.LoginName];
                        user.Name = givenName;
                        user.Update();
                        lblOutput.Text = "</pre>
<span style="color: green;"><b>Display name successfully updated.</b></span>
<pre>
";
                    }
                }
            });
        }
        catch (Exception ex)
        {
            lblOutput.Text = "</pre>
<span style="color: red;"><b>Exception: </b>" + ex.Message + "</span>
<pre>
";
        }
    }
}

There now, that’s better!

26 thoughts on “Using LinkedIn as an Identity Provider for SharePoint 2010

  1. Pingback: Running With Elevated Privileges » Blog Archive » Using LinkedIn as an Identity Provider for SharePoint 2010 « JC’s Blog-O-Gibberish

  2. Johanna Forsberg

    Hi,

    I have tried making this LinkedIn Authentication using your steps on this site, but I can’t get it to work.

    I created a new website, added my sts reference (url: http://sts.contoso.com wich I created in IIS). I have entered the PowerShell commands and when I go to my signin url (http://sharepoint) I get to choose LinkedIn, but then I get the error: http 404 not found.

    What am I doing wrong?

    Reply
          1. Johanna Forsberg

            Hi Danny,

            I checked my Hosts-file and noticed that I’ve missed to enter http://sts.contoso.com there.

            If I add it to the Hosts-file, do you think my problem will go away?

            Thanks for your fast replys!

            // Johanna

          2. Danny Jessee Post author

            Yes, if your IIS web site is configured to use the host header sts.contoso.com and you want to point that to your local machine, you should add a line to the hosts file that looks like this:

            127.0.0.1 sts.contoso.com

            Hope that helps!

  3. Pavan kumar

    Hai,
    I am trying to integrate LinkedIn to my sharepoint 2010 intranet by following your steps.
    Farms authentication is also enabled on my site. By using both windows and Forms authentication i successfully logged in to site. But when select LinkedIn i get the error: HTTP 403 Forbidden and

    The website declined to show this webpage
    HTTP 403
    Most likely causes:
    •This website requires you to log in.

    I think the problem is in the url when its redirected.
    The Url of my iis website is http://tekpc18:7444/
    and the url that i had given in the OAuth Redirect URL box is:
    http://tekpc18:7444/Default.aspx?wa=wsignin1.0&wtrealm=http%3a%2f%2fintranet.tekpc18:7444%2f_trust%2f
    pls help me asap…

    Reply
    1. Danny Jessee Post author

      Pavan – I think you may need to change the “wtrealm” parameter in your OAuth Redirect URL to be tekpc18:7444 instead of intranet.tekpc18:7444. Let me know if that helps.

      Reply
  4. Pingback: Customize the Default #WIF ASP.NET #STS to Support Multiple #SharePoint Web Applications | Running With Elevated Privileges

  5. Istvan

    Hello,

    How can I make the personalization without user interaction? Thus the user log in to the portal and the Display Name automatically overwrite the ID.

    Thx,

    Istvan

    Reply
    1. Danny Jessee

      Unfortunately there is no event you can capture to update the display name automatically the first time a user signs in. You could run a script on a schedule to update the display names for new users (at least removing the user interaction part), but it wouldn’t happen automatically the first time the user signs in.

      Reply
        1. Danny Jessee

          Good find! That has a lot of potential. I believe an HTTPModule would be the only way to accomplish this. If you try it out, let me know how it works for you. Thanks for sharing!

          Reply
          1. Istvan

            Others not suggest HTTPModule method because it caues resource problems. So I write a custom aspx page and place a javascript in a master page, which check the user Name(Title) and if is number(match the regex) redirect the user to the aspx page. (and attach a query string with orignal url, so when the user click the save button, redirect to original link)

            Could you try the search in the loggad on user (on that page, where only the SAML auth enabled)?

            On SP2013 the search not working fairly when I disabled NTLM on the extended site.

            Thanks,
            Istvan

          2. Danny Jessee

            Interesting workaround. Sounds like it gets the job done. You should blog it!

            As far as search goes, if Windows authentication is not selected on any zone of the Web application, crawling will be disabled and search will not work. No way around that.

    1. Danny Jessee

      Thanks, Dee! The ‘nameidentifier’ claim is parsed from the user’s LinkedIn profile URL, which is returned by the LinkedIn API. However, it appears that the format of that URL recently changed from using ‘key=’ to ‘id=’ instead. I updated the code for the GetProfileId(string url) function in my STS code above (see line 78 in the Login class snippet) and verified that profile IDs are being correctly parsed again. Thank you so much for bringing this to my attention!

      Reply
  6. justskeddy

    Hi Danny,

    I’ve tried to work out how best to do this with Google, but I’m getting stuck.

    I’ve used Azure ACS to set up Google as the identity provider, but on the welcome bar it’s displaying the username as “https://www.google.com/accounts/o8/id?id=aaaaaaaaaanaaaan

    Ideally I’d like to use a PowerShell script to quickly change this permanently.

    Are you able to provide a way to do this? Within the rule of ACS, I’ve got the following claims being passed through:

    emailaddress
    name
    nameidentifier

    If you can help me quickly change this, that would be awesome.

    Thanks!

    Rob

    Reply
    1. justskeddy

      Hi Danny,

      Thought this might also help – this is the powershell script that I used to successfully add in the authentication.

      # Add snapin
      add-pssnapin microsoft.sharepoint.powershell -ErrorAction SilentlyContinue

      # Create service and attached non-encypted X.509 certificate
      $certloc = “D:NameOfCertificte.cer”

      $rootcert = Get-PfxCertificate $certloc

      New-SPTrustedRootAuthority -Name “Google Authentication Signing Certificate” -Certificate $rootcert | Out-Null

      $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($certloc)

      $map1 = New-SPClaimTypeMapping -IncomingClaimType “http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier” -IncomingClaimTypeDisplayName “UPN” -LocalClaimType “http://schemas.xmlsoap.org/claims/UPN”

      $map2 = New-SPClaimTypeMapping -IncomingClaimType “http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress” -IncomingClaimTypeDisplayName “Email Address” -SameAsIncoming

      $realm = “https://nameofmywebsite.com/”

      $signInUrl = “https://nameofmy.accesscontrol.windows.net/v2/wsfederation”

      $ap = New-SPTrustedIdentityTokenIssuer -Name “Google Authentication” -Description “Google Authentication” -Realm $realm -ImportTrustCertificate $cert -ClaimsMappings $map1,$map2 -SignInUrl $signInUrl -IdentifierClaim “http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier“

      Reply
    2. Danny Jessee

      I don’t have a ready-made PowerShell script to do this, but you should be able to adapt what I did in my LinkedInNameChanger class above in conjunction with the Set-SPUser -Identity [identity] -DisplayName [displayname] -Web [url] cmdlet to accomplish this.

      Reply
  7. Pingback: Project Server Blog » SharePoint hosted Apps with SAML authentication

  8. Pingback: » Using LinkedIn as an Identity Provider for SharePoint 2010

  9. Pingback: Nearbaseline

  10. spon

    Hi Danny,

    This is a good post, thank you.

    The call to :

    FederatedPassiveSecurityTokenServiceOperations.ProcessRequest
    throws UnauthorizedException.
    I am first testing with a normal webApp as an RP before moving to SP2013.

    [UnauthorizedAccessException: Attempted to perform an unauthorized operation.]
    System.IdentityModel.Services.FederatedPassiveSecurityTokenServiceOperations.ProcessRequest(HttpRequest request, ClaimsPrincipal principal, SecurityTokenService sts, HttpResponse response, WSFederationSerializer federationSerializer)

    Can you help me with this ?
    Thanks !!

    Reply

Leave a Reply