Skip to content

Commit

Permalink
Update LinkedIn provider to V2 API
Browse files Browse the repository at this point in the history
Updated LinkedIn OAuth Provider to new version 2 format and endpoints.
This r_liteprofile drops some fields which were previously provided, so you will no longer have access to user.Description or user.Location.
  • Loading branch information
baloo32 committed May 24, 2019
1 parent 10f4d88 commit e7b368f
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 28 deletions.
152 changes: 126 additions & 26 deletions providers/linkedin/linkedin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,27 @@ package linkedin
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"

"fmt"
"github.com/markbates/goth"
"golang.org/x/oauth2"
)

//more details about linkedin fields: https://developer.linkedin.com/documents/profile-fields
// more details about linkedin fields:
// User Profile and Email Address - https://docs.microsoft.com/en-gb/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin
// User Avatar - https://docs.microsoft.com/en-gb/linkedin/shared/references/v2/digital-media-asset

const (
authURL string = "https://www.linkedin.com/oauth/v2/authorization"
tokenURL string = "https://www.linkedin.com/oauth/v2/accessToken"

//userEndpoint requires scopes "r_basicprofile", "r_emailaddress"
userEndpoint string = "//api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,location:(name),picture-url,email-address)"
//userEndpoint requires scope "r_liteprofile"
userEndpoint string = "//api.linkedin.com/v2/me?projection=(id,firstName,lastName,profilePicture(displayImage~:playableStreams))"
//emailEndpoint requires scope "r_emailaddress"
emailEndpoint string = "//api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))"
)

// New creates a new linkedin provider, and sets up important connection details.
Expand Down Expand Up @@ -57,6 +61,7 @@ func (p *Provider) SetName(name string) {
p.providerName = name
}

// Client returns an HTTPClientWithFallback
func (p *Provider) Client() *http.Client {
return goth.HTTPClientWithFallBack(p.HTTPClient)
}
Expand Down Expand Up @@ -87,63 +92,153 @@ func (p *Provider) FetchUser(session goth.Session) (goth.User, error) {
return user, fmt.Errorf("%s cannot get user information without accessToken", p.providerName)
}

// create request for user r_liteprofile
req, err := http.NewRequest("GET", "", nil)
if err != nil {
return user, err
}

//add url as opaque to avoid escaping of "("
// add url as opaque to avoid escaping of "("
req.URL = &url.URL{
Scheme: "https",
Host: "api.linkedin.com",
Opaque: userEndpoint,
}

req.Header.Set("Authorization", "Bearer "+s.AccessToken)
req.Header.Add("x-li-format", "json") //request json response
resp, err := p.Client().Do(req)
if err != nil {
return user, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user information", p.providerName, resp.StatusCode)
return user, fmt.Errorf("%s responded with a %d trying to fetch user profile", p.providerName, resp.StatusCode)
}

//err = userFromReader(io.TeeReader(resp.Body, os.Stdout), &user)
// read r_liteprofile information
err = userFromReader(resp.Body, &user)
if err != nil {
return user, err
}

// create request for user r_emailaddress
reqEmail, err := http.NewRequest("GET", "", nil)
if err != nil {
return user, err
}

// add url as opaque to avoid escaping of "("
reqEmail.URL = &url.URL{
Scheme: "https",
Host: "api.linkedin.com",
Opaque: emailEndpoint,
}

reqEmail.Header.Set("Authorization", "Bearer "+s.AccessToken)
respEmail, err := p.Client().Do(reqEmail)
if err != nil {
return user, err
}
defer respEmail.Body.Close()

if respEmail.StatusCode != http.StatusOK {
return user, fmt.Errorf("%s responded with a %d trying to fetch user email", p.providerName, respEmail.StatusCode)
}

// read r_emailaddress information
err = emailFromReader(respEmail.Body, &user)

return user, err
}

func userFromReader(reader io.Reader, user *goth.User) error {

u := struct {
ID string `json:"id"`
Email string `json:"emailAddress"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Headline string `json:"headline"`
PictureURL string `json:"pictureUrl"`
Location struct {
Name string `json:"name"`
} `json:"location"`
ID string `json:"id"`
FirstName struct {
PreferredLocale struct {
Country string `json:"country"`
Language string `json:"language"`
} `json:"preferredLocale"`
Localized map[string]string `json:"localized"`
} `json:"firstName"`
LastName struct {
Localized map[string]string
PreferredLocale struct {
Country string `json:"country"`
Language string `json:"language"`
} `json:"preferredLocale"`
} `json:"lastName"`
ProfilePicture struct {
DisplayImage struct {
Elements []struct {
AuthorizationMethod string `json:"authorizationMethod"`
Identifiers []struct {
Identifier string `json:"identifier"`
IdentifierType string `json:"identifierType"`
} `json:"identifiers"`
} `json:"elements"`
} `json:"displayImage~"`
} `json:"profilePicture"`
}{}

err := json.NewDecoder(reader).Decode(&u)
if err != nil {
return err
}

user.FirstName = u.FirstName
user.LastName = u.LastName
user.Name = u.FirstName + " " + u.LastName
user.NickName = u.FirstName
user.Email = u.Email
user.Description = u.Headline
user.AvatarURL = u.PictureURL
user.FirstName = u.FirstName.Localized[u.FirstName.PreferredLocale.Language+"_"+u.FirstName.PreferredLocale.Country]
user.LastName = u.LastName.Localized[u.LastName.PreferredLocale.Language+"_"+u.LastName.PreferredLocale.Country]
user.Name = user.FirstName + " " + user.LastName
user.NickName = user.FirstName
user.UserID = u.ID
user.Location = u.Location.Name

avatarURL := ""
// loop all displayimage elements
for _, element := range u.ProfilePicture.DisplayImage.Elements {
// only retrieve data where the authorization method allows public (unauthorized) access
if element.AuthorizationMethod == "PUBLIC" {
for _, identifier := range element.Identifiers {
// check to ensure the identifer type is a url linking to the image
if identifier.IdentifierType == "EXTERNAL_URL" {
avatarURL = identifier.Identifier
// we only need the first image url
break
}
}
}
// if we have a valid image, exit the loop as we only support a single avatar image
if len(avatarURL) > 0 {
break
}
}

user.AvatarURL = avatarURL

return err
}

func emailFromReader(reader io.Reader, user *goth.User) error {
e := struct {
Elements []struct {
Handle struct {
EmailAddress string `json:"emailAddress"`
} `json:"handle~"`
} `json:"elements"`
}{}

err := json.NewDecoder(reader).Decode(&e)
if err != nil {
return err
}

if len(e.Elements) > 0 {
user.Email = e.Elements[0].Handle.EmailAddress
}

if len(user.Email) == 0 {
return errors.New("Unable to retrieve email address")
}

return err
}
Expand All @@ -160,6 +255,11 @@ func newConfig(provider *Provider, scopes []string) *oauth2.Config {
Scopes: []string{},
}

if len(scopes) == 0 {
// add helper as new API requires the scope to be specified and these are the minimum to retrieve profile information and user's email address
scopes = append(scopes, "r_liteprofile", "r_emailaddress")
}

for _, scope := range scopes {
c.Scopes = append(c.Scopes, scope)
}
Expand Down
4 changes: 2 additions & 2 deletions providers/linkedin/linkedin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func Test_BeginAuth(t *testing.T) {
a.Contains(s.AuthURL, "linkedin.com/oauth/v2/authorization")
a.Contains(s.AuthURL, fmt.Sprintf("client_id=%s", os.Getenv("LINKEDIN_KEY")))
a.Contains(s.AuthURL, "state=test_state")
a.Contains(s.AuthURL, "scope=r_basicprofile+r_emailaddress&state")
a.Contains(s.AuthURL, "scope=r_liteprofile+r_emailaddress&state")
}

func Test_SessionFromJSON(t *testing.T) {
Expand All @@ -55,5 +55,5 @@ func Test_SessionFromJSON(t *testing.T) {
}

func linkedinProvider() *linkedin.Provider {
return linkedin.New(os.Getenv("LINKEDIN_KEY"), os.Getenv("LINKEDIN_SECRET"), "/foo", "r_basicprofile", "r_emailaddress")
return linkedin.New(os.Getenv("LINKEDIN_KEY"), os.Getenv("LINKEDIN_SECRET"), "/foo", "r_liteprofile", "r_emailaddress")
}

0 comments on commit e7b368f

Please sign in to comment.