Skip to content
This repository has been archived by the owner on Sep 12, 2021. It is now read-only.

Commit

Permalink
Fixed Profile URL as v1 resource is no longer available (#568)
Browse files Browse the repository at this point in the history
* Fixed Profile URL as v1 resource is no longer available
* Fixed testing and retrived the full profile from LinkedIn
  • Loading branch information
ViniciusMiana authored and akkie committed Oct 10, 2019
1 parent dbc6b55 commit 49fc9c7
Show file tree
Hide file tree
Showing 6 changed files with 503 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import com.mohiva.play.silhouette.api.util.HTTPLayer
import com.mohiva.play.silhouette.impl.exceptions.ProfileRetrievalException
import com.mohiva.play.silhouette.impl.providers._
import com.mohiva.play.silhouette.impl.providers.oauth2.LinkedInProvider._
import play.api.libs.json.JsValue
import play.api.libs.json.{ JsObject, JsValue }

import scala.concurrent.Future

Expand All @@ -50,17 +50,15 @@ trait BaseLinkedInProvider extends OAuth2Provider {
/**
* Defines the URLs that are needed to retrieve the profile data.
*/
override protected val urls = Map("api" -> settings.apiURL.getOrElse(API))

/**
* Builds the social profile.
*
* @param authInfo The auth info received from the provider.
* @return On success the build social profile, otherwise a failure.
*/
override protected def buildProfile(authInfo: OAuth2Info): Future[Profile] = {
httpLayer.url(urls("api").format(authInfo.accessToken)).get().flatMap { response =>
val json = response.json
override protected val urls = Map(
"api" -> settings.apiURL.getOrElse(API),
"email" -> settings.customProperties.getOrElse("emailURL", EMAIL),
"photo" -> settings.customProperties.getOrElse("photoURL", PHOTO)
)

private def getPartialProfile(url: String, authInfo: OAuth2Info): Future[JsValue] = {
httpLayer.url(url.format(authInfo.accessToken)).get().flatMap { response =>
val json: JsValue = response.json
(json \ "errorCode").asOpt[Int] match {
case Some(error) =>
val message = (json \ "message").asOpt[String]
Expand All @@ -69,10 +67,23 @@ trait BaseLinkedInProvider extends OAuth2Provider {
val timestamp = (json \ "timestamp").asOpt[Long]

Future.failed(new ProfileRetrievalException(SpecifiedProfileError.format(id, error, message, requestId, status, timestamp)))
case _ => profileParser.parse(json, authInfo)
case _ => Future.successful(json)
}
}
}
/**
* Builds the social profile.
*
* @param authInfo The auth info received from the provider.
* @return On success the build social profile, otherwise a failure.
*/
override protected def buildProfile(authInfo: OAuth2Info): Future[Profile] = {
Future.sequence(Seq(getPartialProfile(urls("api"), authInfo), getPartialProfile(urls("email"), authInfo), getPartialProfile(urls("photo"), authInfo))).flatMap {
partial =>
val array: JsValue = JsObject(Seq("api" -> partial(0), "email" -> partial(1), "photo" -> partial(2)))
profileParser.parse(array, authInfo)
}
}
}

/**
Expand All @@ -88,12 +99,12 @@ class LinkedInProfileParser extends SocialProfileParser[JsValue, CommonSocialPro
* @return The social profile from given result.
*/
override def parse(json: JsValue, authInfo: OAuth2Info) = Future.successful {
val userID = (json \ "id").as[String]
val firstName = (json \ "firstName").asOpt[String]
val lastName = (json \ "lastName").asOpt[String]
val fullName = (json \ "formattedName").asOpt[String]
val avatarURL = (json \ "pictureUrl").asOpt[String]
val email = (json \ "emailAddress").asOpt[String]
val userID = (json \ "api" \ "id").as[String]
val firstName = (json \ "api" \ "localizedFirstName").asOpt[String]
val lastName = (json \ "api" \ "localizedLastName").asOpt[String]
val fullName = Some(firstName.getOrElse("") + " " + lastName.getOrElse("")).map(_.trim)
val avatarURL = (json \\ "identifier")(0).asOpt[String]
val email = (json \\ "emailAddress")(0).asOpt[String]

CommonSocialProfile(
loginInfo = LoginInfo(ID, userID),
Expand Down Expand Up @@ -151,5 +162,7 @@ object LinkedInProvider {
* The LinkedIn constants.
*/
val ID = "linkedin"
val API = "https://api.linkedin.com/v1/people/~:(id,first-name,last-name,formatted-name,picture-url,email-address)?format=json&oauth2_access_token=%s"
val API = "https://api.linkedin.com/v2/me?oauth2_access_token=%s"
val EMAIL = "https://api.linkedin.com/v2/clientAwareMemberHandles?q=members&projection=(elements*(primary,type,handle~))&oauth2_access_token=%s"
val PHOTO = "https://api.linkedin.com/v2/me?projection=(id,profilePicture(displayImage~:playableStreams))&oauth2_access_token=%s"
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package com.mohiva.play.silhouette.impl.providers.oauth2

import com.mohiva.play.silhouette.api.LoginInfo
import com.mohiva.play.silhouette.api.util.{ ExtractableRequest, MockWSRequest }
import com.mohiva.play.silhouette.api.util.{ ExtractableRequest, MockHTTPLayer, MockWSRequest }
import com.mohiva.play.silhouette.impl.exceptions.{ ProfileRetrievalException, UnexpectedResponseException }
import com.mohiva.play.silhouette.impl.providers.OAuth2Provider._
import com.mohiva.play.silhouette.impl.providers.SocialProfileBuilder._
Expand Down Expand Up @@ -121,7 +121,7 @@ class LinkedInProviderSpec extends OAuth2ProviderSpec {
wsResponse.json returns Helper.loadJson("providers/oauth2/linkedin.error.json")
wsRequest.get() returns Future.successful(wsResponse)
httpLayer.url(API.format("my.access.token")) returns wsRequest

mockEmailAndPhoto(httpLayer)
failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) {
case e => e.getMessage must equalTo(SpecifiedProfileError.format(
provider.id,
Expand All @@ -140,7 +140,7 @@ class LinkedInProviderSpec extends OAuth2ProviderSpec {
wsResponse.json throws new RuntimeException("")
wsRequest.get() returns Future.successful(wsResponse)
httpLayer.url(API.format("my.access.token")) returns wsRequest

mockEmailAndPhoto(httpLayer)
failed[ProfileRetrievalException](provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) {
case e => e.getMessage must equalTo(UnspecifiedProfileError.format(provider.id))
}
Expand All @@ -155,28 +155,49 @@ class LinkedInProviderSpec extends OAuth2ProviderSpec {
wsResponse.json returns Helper.loadJson("providers/oauth2/linkedin.success.json")
wsRequest.get() returns Future.successful(wsResponse)
httpLayer.url(url.format("my.access.token")) returns wsRequest

mockEmailAndPhoto(httpLayer)
await(provider.retrieveProfile(oAuthInfo.as[OAuth2Info]))

there was one(httpLayer).url(url.format("my.access.token"))
}

def mockEmailAndPhoto(httpLayer: MockHTTPLayer) = {
// Email
val wsRequestEmail = mock[MockWSRequest]
val wsResponseEmail = mock[MockWSRequest#Response]
wsResponseEmail.status returns 200
wsResponseEmail.json returns Helper.loadJson("providers/oauth2/linkedin.email.json")
wsRequestEmail.get() returns Future.successful(wsResponseEmail)
httpLayer.url(EMAIL.format("my.access.token")) returns wsRequestEmail
// Photo
val wsRequestPhoto = mock[MockWSRequest]
val wsResponsePhoto = mock[MockWSRequest#Response]
wsResponsePhoto.status returns 200
wsResponsePhoto.json returns Helper.loadJson("providers/oauth2/linkedin.photo.json")
wsRequestPhoto.get() returns Future.successful(wsResponsePhoto)
httpLayer.url(PHOTO.format("my.access.token")) returns wsRequestPhoto

}

"return the social profile" in new WithApplication with Context {
// Basic profile
val wsRequest = mock[MockWSRequest]
val wsResponse = mock[MockWSRequest#Response]
wsResponse.status returns 200
wsResponse.json returns Helper.loadJson("providers/oauth2/linkedin.success.json")
wsRequest.get() returns Future.successful(wsResponse)
httpLayer.url(API.format("my.access.token")) returns wsRequest

mockEmailAndPhoto(httpLayer)

profile(provider.retrieveProfile(oAuthInfo.as[OAuth2Info])) { p =>
p must be equalTo CommonSocialProfile(
loginInfo = LoginInfo(provider.id, "NhZXBl_O6f"),
firstName = Some("Apollonia"),
lastName = Some("Vanova"),
fullName = Some("Apollonia Vanova"),
email = Some("apollonia.vanova@watchmen.com"),
avatarURL = Some("http://media.linkedin.com/mpr/mprx/0_fsPnURNRhLhk_Ue2fjKLUZkB2FL6TOe2S4bdUZz61GA9Ysxu_y_sz4THGW5JGJWhaMleN0F61-Dg")
avatarURL = Some("https://media.licdn.com/dms/image/C4E03AQFBprjocrF2iA/profile-displayphoto-shrink_100_100/0?e=1576108800&v=beta&t=Tn7mA43w8qmTuzjSdtuYQMi2kI5At9XOp8X--s5hpRU")
)
}
}
Expand Down
12 changes: 12 additions & 0 deletions silhouette/test/resources/providers/oauth2/linkedin.email.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"elements": [
{
"handle": "urn:li:emailAddress:30919315",
"type": "EMAIL",
"handle~": {
"emailAddress": "apollonia.vanova@watchmen.com"
},
"primary": true
}
]
}
Loading

0 comments on commit 49fc9c7

Please sign in to comment.