Consume LinkedIn API with Golang

Or how to build an OAuth2 client with go

Francisco Escher
6 min readFeb 21, 2023

TLDR

This story presents the implementation of a golang package for calling the LinkedIn API v2 using OAuth2. It can be extrapolated to other OAuth2 APIs.

The full code can be found at this this Github link.

Currently, it only implements the endpoints /me, /clientAwareMemberHandles, /people, and /emailAddress.

Introduction

LinkedIn API v2 uses OAuth2 so a user can grant permission over their data to external applications.

LinkedIn does not provide a native golang package to use this API, but luckily golang has a nice OAuth2 package (golang.org/x/oauth2) to help us make use of it. I will show my implementation here.

To use this package you will need a LinkedIn App registered (You can do so at this link). In that same page, you can get API credentials (client id and secret) for this app, and also access to the OAuth2 scopes you want to use. If you are not sure what are those scopes, take a look at this link.

You will also need a redirect route so that LinkedIn will redirect the user who grants permission for the application. This route must be registered as an “Authorized redirect URLs for your app” on your app page, or you will get an error while trying to use it.

Development

The steps for using the API are the following:

1. Decide the type of data you want to query

Depending on this type, you will need to use different scopes and endpoints. Check this link to know more about them.

2. Create an authorization URL

Using the OAuth2 go package:

conf := &oauth2.Config{
ClientID: "<client_id>",
ClientSecret: "<client_secret",
Scopes: []string{"r_liteprofile", "r_emailaddress"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://www.linkedin.com/oauth/v2/authorization",
TokenURL: "https://www.linkedin.com/oauth/v2/accessToken",
AuthStyle: oauth2.AuthStyleInParams,
},
RedirectURL: "https://localhost:8080/auth/linkedin/callback",
}

which should result in something like:

https://www.linkedin.com/oauth/v2/authorization?access_type=offline&client_id=%3Cclient_id%3E&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Flinkedin%2Fcallback&response_type=code&scope=r_liteprofile+r_emailaddress&state=%3Cstate

The user must sign in to LinkedIn and accept the use of their data using this link. He will be redirected to the address in the RedirectURL field.

Create an HTTP client using the given code

The user will be redirected to the address in the RedirectURL field, with the URL query parameters state and code.

The state is the same value you passed to generate the auth URL and should be used by you as a CSRF token (not in scope for us here).

First, retrieve a token using the OAuth2 configuration (with the same parameters you used before). Then, create an HTTP client with that same configuration and token (the first parameter of those methods is a context, you can pass any you prefer, something like context.Background() should work).

// code is the code returned by the linkedin callback as a query parameter
code := r.URL.Query().Get("code")
// conf shoud be build with the same parameters as before
token, err := conf.Exchange(ctx, code)
if err != nil {
return nil, err
}
client := conf.Client(ctx, token)

Note: in case there is any error with the authentication, your callback will be called with the query parameters error and error_description instead.

Note 2: this use case us a generic use of OAuth2, and some other APIs using it could possibly be implemented using this same approach.

Call the desired endpoint using the HTTP client

There is not much secret in this step. In the example below I’ve called the emailAddress endpoint and decoded the response into a struct that follows the API format.

const EndpointEmailAddress = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"


// response structure
type EmailAddress struct {
ErrorResponse
Elements []struct {
Handle string `json:"handle"`
HandleTilde struct {
EmailAddress string `json:"emailAddress"`
} `json:"handle~"`
} `json:"elements"`
}

// possible error response
type ErrorResponse struct {
ServiceErrorCode int `json:"serviceErrorCode"`
Message string `json:"message"`
Status int `json:"status"`
}

// calls the client
resp, err := client.Get(EndpointAddress)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()

// parses the response
var r EmailAddress
err = json.NewDecoder(resp.Body).Decode(&r)
if err != nil {
log.Fatal(err)
}

// prints parsed result
fmt.Println(r)

Wrapping up into a package

I found it was a cleaner solution to wrap up everything into 2 structs, a Builder and a Client.

The builder is responsible for creating the auth URL and the Client, as it shows below:

package golinkedin

import (
"context"

"golang.org/x/oauth2"
)

var (
oauth2endpoint = oauth2.Endpoint{
AuthURL: "https://www.linkedin.com/oauth/v2/authorization",
TokenURL: "https://www.linkedin.com/oauth/v2/accessToken",
AuthStyle: oauth2.AuthStyleInParams,
}
)

// NewBuilder returns a new linkedin client, not yet authenticated.
func NewBuilder(clientID string, clientSecret string, scopes []string, redirectURL string) *Builder {
return &Builder{
clientID: clientID,
clientSecret: clientSecret,
scopes: scopes,
redirectURL: redirectURL,
}
}

type Builder struct {
// ClientID is the api key client's ID.
clientID string
// ClientSecret is the api key client's secret.
clientSecret string
// Scopes is the list of scopes that the client will request.
scopes []string
// redirectURL is the URL that the user will be redirected to after
// authenticating with linkedin in the GetAuthURL url response.
redirectURL string
}

// getOAuth2Config returns the oauth2 config
func (c *Builder) getOAuth2Config() *oauth2.Config {
conf := &oauth2.Config{
ClientID: c.clientID,
ClientSecret: c.clientSecret,
Scopes: c.scopes,
Endpoint: oauth2endpoint,
RedirectURL: c.redirectURL,
}
return conf
}

// GetAuthURL returns the URL to the linkedin login page
// The state is a string that will be returned to the redirect URL
// so it can be used to prevent CSRF attacks
func (c *Builder) GetAuthURL(state string) string {
oa2config := c.getOAuth2Config()
url := oa2config.AuthCodeURL(state, oauth2.AccessTypeOffline)
return url
}

// GetClient will exchange the code for an access token and
// use it to create a new client with an authorized http client
func (c *Builder) GetClient(ctx context.Context, code string) (*Client, error) {
oa2config := c.getOAuth2Config()
token, err := oa2config.Exchange(ctx, code)
if err != nil {
return nil, err
}
client := oa2config.Client(ctx, token)
new := &Client{*client}
return new, nil
}

The client still should be used to call the API endpoints, but now has the API methods integrated into it, as below:

package golinkedin

import (
"net/http"
"encoding/json"
)

type Client struct {
http.Client
}

// Common struct for error response in linkedin API
type ErrorResponse struct {
ServiceErrorCode int `json:"serviceErrorCode"`
Message string `json:"message"`
Status int `json:"status"`
}

const EndpointEmailAddress = "https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"

// EmailAddressRequest calls emailAddress api.
// Please note that email address is only available with the scope r_emailaddress.
func (c *Client) EmailAddressRequest() (resp *http.Response, err error) {
return c.Get(EndpointEmailAddress)
}

// Same as EmailAddressRequest but parses the response.
func (c *Client) GetEmailAddress() (r EmailAddress, err error) {
resp, err := c.EmailAddressRequest()
if err != nil {
return r, err
}
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&r)
return r, err
}

type EmailAddress struct {
ErrorResponse
Elements []struct {
Handle string `json:"handle"`
HandleTilde struct {
EmailAddress string `json:"emailAddress"`
} `json:"handle~"`
} `json:"elements"`
}

And now we can use the package just as simple as below:

package main

import (
"context"
"fmt"

"github.com/franciscoescher/golinkedin"
)

func main() {
// first, you need to create a builder with your api key credentials
scopes := []string{"r_liteprofile", "r_emailaddress"}
builder := golinkedin.NewBuilder("CLIENT_ID", "CLIENT_SECRET", scopes, "REDIRECT_URL")

// then, you need to get the auth url to redirect the user to the linkedin login page
url := builder.GetAuthURL("state")
fmt.Println(url)

// after user accepts, linkedin will redirect to REDIRECT_URL with a code parameter
// you need to use this code to authenticate and get the client
code := "CODE"
client, err := builder.GetClient(context.Background(), code)
if err != nil {
log.Fatal(err)
}

// now you can use the client to make requests
response, err := client.GetEmailAddress()
if err != nil {
log.Fatal(err)
}
fmt.Println(response)
}

Conclusion

Overall, golang provides a quite simple implementation of the OAuth2 protocol. It was quite simple to call the LinkedIn API using it. although the API itself can be quite annoying to understand (especially because of its documentation). But in the end, the package created was quite clean and easy to use.

--

--