Skip to content

Commit

Permalink
Merge pull request #30 from gamfi/feature/attempt-to-set-email-in-res…
Browse files Browse the repository at this point in the history
…ource-owner

Attempt to set email in resource owner
  • Loading branch information
stevenmaguire committed May 22, 2019
2 parents 3d68f35 + 4fcdbf7 commit 9e9ea27
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 16 deletions.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -23,7 +23,8 @@
"require-dev": {
"phpunit/phpunit": "~4.0",
"mockery/mockery": "~0.9",
"squizlabs/php_codesniffer": "~2.0"
"squizlabs/php_codesniffer": "~2.0",
"ext-json": "*"
},
"autoload": {
"psr-4": {
Expand Down
8 changes: 8 additions & 0 deletions src/Provider/Exception/LinkedInAccessDeniedException.php
@@ -0,0 +1,8 @@
<?php

namespace League\OAuth2\Client\Provider\Exception;

class LinkedInAccessDeniedException extends IdentityProviderException
{

}
30 changes: 25 additions & 5 deletions src/Provider/LinkedIn.php
Expand Up @@ -6,6 +6,7 @@
use InvalidArgumentException;
use League\OAuth2\Client\Grant\AbstractGrant;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Provider\Exception\LinkedInAccessDeniedException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Token\LinkedInAccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
Expand Down Expand Up @@ -150,15 +151,24 @@ protected function getDefaultScopes()
*
* @throws IdentityProviderException
* @param ResponseInterface $response
* @param string $data Parsed response data
* @param array $data Parsed response data
* @return void
*/
protected function checkResponse(ResponseInterface $response, $data)
{
if (isset($data['error'])) {
// https://developer.linkedin.com/docs/guide/v2/error-handling
if ($response->getStatusCode() >= 400) {
if (isset($data['status']) && $data['status'] === 403) {
throw new LinkedInAccessDeniedException(
$data['message'] ?: $response->getReasonPhrase(),
$response->getStatusCode(),
$response
);
}

throw new IdentityProviderException(
$data['error_description'] ?: $response->getReasonPhrase(),
$response->getStatusCode(),
$data['message'] ?: $response->getReasonPhrase(),
$data['status'] ?: $response->getStatusCode(),
$response
);
}
Expand All @@ -173,7 +183,17 @@ protected function checkResponse(ResponseInterface $response, $data)
*/
protected function createResourceOwner(array $response, AccessToken $token)
{
return new LinkedInResourceOwner($response);
// If current accessToken is not authorized with r_emailaddress scope,
// getResourceOwnerEmail will throw LinkedInAccessDeniedException, it will be caught here,
// and then the email will be set to null
// When email is not available due to chosen scopes, other providers simply set it to null, let's do the same.
try {
$email = $this->getResourceOwnerEmail($token);
} catch (LinkedInAccessDeniedException $exception) {
$email = null;
}

return new LinkedInResourceOwner($response, $email);
}

/**
Expand Down
19 changes: 18 additions & 1 deletion src/Provider/LinkedInResourceOwner.php
Expand Up @@ -25,14 +25,21 @@ class LinkedInResourceOwner extends GenericResourceOwner
*/
protected $sortedProfilePictures = [];

/**
* @var string|null
*/
private $email;

/**
* Creates new resource owner.
*
* @param array $response
* @param string|null $email
*/
public function __construct(array $response = array())
public function __construct(array $response = array(), $email = null)
{
$this->response = $response;
$this->email = $email;
$this->setSortedProfilePictures();
}

Expand Down Expand Up @@ -138,6 +145,16 @@ public function getUrl()
return $vanityName ? sprintf('https://www.linkedin.com/in/%s', $vanityName) : null;
}

/**
* Get user email, if available
*
* @return string|null
*/
public function getEmail()
{
return $this->email;
}

/**
* Attempts to sort the collection of profile pictures included in the profile
* before caching them in the resource owner instance.
Expand Down
104 changes: 95 additions & 9 deletions test/src/Provider/LinkedInTest.php
Expand Up @@ -126,6 +126,7 @@ public function testGetAccessToken()
$response = m::mock('Psr\Http\Message\ResponseInterface');
$response->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token", "expires_in": 3600, "refresh_token": "mock_refresh_token", "refresh_token_expires_in": 7200}');
$response->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$response->shouldReceive('getStatusCode')->andReturn(200);

$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')->times(1)->andReturn($response);
Expand All @@ -145,21 +146,29 @@ public function testGetAccessToken()
public function testUserData()
{
$apiProfileResponse = json_decode(file_get_contents(__DIR__.'/../../api_responses/me.json'), true);
$apiEmailResponse = json_decode(file_get_contents(__DIR__.'/../../api_responses/email.json'), true);
$somethingExtra = ['more' => uniqid()];
$apiProfileResponse['somethingExtra'] = $somethingExtra;

$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token", "expires_in": 3600}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);

$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn(json_encode($apiProfileResponse));
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$userResponse->shouldReceive('getStatusCode')->andReturn(200);

$emailResponse = m::mock('Psr\Http\Message\ResponseInterface');
$emailResponse->shouldReceive('getBody')->andReturn(json_encode($apiEmailResponse));
$emailResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$emailResponse->shouldReceive('getStatusCode')->andReturn(200);

$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
->times(2)
->andReturn($postResponse, $userResponse);
->times(3)
->andReturn($postResponse, $userResponse, $emailResponse);
$this->provider->setHttpClient($client);

$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
Expand All @@ -173,6 +182,7 @@ public function testUserData()
$this->assertEquals('Doe', $user->toArray()['localizedLastName']);
$this->assertEquals('http://example.com/avatar_800_800.jpeg', $user->getImageUrl());
$this->assertEquals('https://www.linkedin.com/in/john-doe', $user->getUrl());
$this->assertEquals('resource-owner@example.com', $user->getEmail());
$this->assertEquals($somethingExtra, $user->getAttribute('somethingExtra'));
$this->assertEquals($somethingExtra, $user->toArray()['somethingExtra']);
$this->assertEquals($somethingExtra['more'], $user->getAttribute('somethingExtra.more'));
Expand All @@ -187,6 +197,7 @@ public function testMissingUserData()
$firstName = uniqid();
$lastName = uniqid();
$apiProfileResponse = json_decode(file_get_contents(__DIR__.'/../../api_responses/me.json'), true);
$apiEmailResponse = json_decode(file_get_contents(__DIR__.'/../../api_responses/email.json'), true);
$apiProfileResponse['id'] = $userId;
$apiProfileResponse['localizedFirstName'] = $firstName;
$apiProfileResponse['localizedLastName'] = $lastName;
Expand All @@ -196,15 +207,22 @@ public function testMissingUserData()
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token", "expires_in": 3600}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);

$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn(json_encode($apiProfileResponse));
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$userResponse->shouldReceive('getStatusCode')->andReturn(200);

$emailResponse = m::mock('Psr\Http\Message\ResponseInterface');
$emailResponse->shouldReceive('getBody')->andReturn(json_encode($apiEmailResponse));
$emailResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$emailResponse->shouldReceive('getStatusCode')->andReturn(200);

$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
->times(2)
->andReturn($postResponse, $userResponse);
->times(3)
->andReturn($postResponse, $userResponse, $emailResponse);
$this->provider->setHttpClient($client);

$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
Expand All @@ -227,10 +245,12 @@ public function testUserEmail()
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token", "expires_in": 3600}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);

$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn(json_encode($apiEmailResponse));
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$userResponse->shouldReceive('getStatusCode')->andReturn(200);

$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
Expand All @@ -250,15 +270,17 @@ public function testUserEmailNullIfApiResponseInvalid()
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token", "expires_in": 3600}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);

$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn(json_encode($apiEmailResponse));
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$emailResponse = m::mock('Psr\Http\Message\ResponseInterface');
$emailResponse->shouldReceive('getBody')->andReturn(json_encode($apiEmailResponse));
$emailResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$emailResponse->shouldReceive('getStatusCode')->andReturn(200);

$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
->times(2)
->andReturn($postResponse, $userResponse);
->andReturn($postResponse, $emailResponse);
$this->provider->setHttpClient($client);

$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
Expand All @@ -268,6 +290,70 @@ public function testUserEmailNullIfApiResponseInvalid()
}
}

public function testResourceOwnerEmailNullWhenNotAuthorized()
{
$apiProfileResponse = json_decode(file_get_contents(__DIR__.'/../../api_responses/me.json'), true);

$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token", "expires_in": 3600}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);

$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')->andReturn(json_encode($apiProfileResponse));
$userResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$userResponse->shouldReceive('getStatusCode')->andReturn(200);

$emailResponse = m::mock('Psr\Http\Message\ResponseInterface');
$emailResponse->shouldReceive('getBody')->andReturn('{"message": "Not enough permissions to access: GET-members /clientAwareMemberHandles","status":403,"serviceErrorCode":100}');
$emailResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$emailResponse->shouldReceive('getStatusCode')->andReturn(403);

$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
->times(3)
->andReturn($postResponse, $userResponse, $emailResponse);
$this->provider->setHttpClient($client);

$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);

$user = $this->provider->getResourceOwner($token);

$this->assertNull($user->getEmail());

$this->assertEquals('abcdef1234', $user->getId());
$this->assertEquals('John', $user->getFirstName());
$this->assertEquals('Doe', $user->getLastName());
$this->assertEquals('http://example.com/avatar_800_800.jpeg', $user->getImageUrl());
$this->assertEquals('https://www.linkedin.com/in/john-doe', $user->getUrl());
}

/**
* @expectedException League\OAuth2\Client\Provider\Exception\LinkedInAccessDeniedException
*/
public function testExceptionThrownWhenEmailIsNotAuthorizedButRequestedFromAdapter()
{
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('{"access_token": "mock_access_token", "expires_in": 3600}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$postResponse->shouldReceive('getStatusCode')->andReturn(200);

$emailResponse = m::mock('Psr\Http\Message\ResponseInterface');
$emailResponse->shouldReceive('getBody')->andReturn('{"message": "Not enough permissions to access: GET-members /clientAwareMemberHandles","status":403,"serviceErrorCode":100}');
$emailResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$emailResponse->shouldReceive('getStatusCode')->andReturn(403);

$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
->times(2)
->andReturn($postResponse, $emailResponse);
$this->provider->setHttpClient($client);

$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);

$this->provider->getResourceOwnerEmail($token);
}

/**
* @expectedException League\OAuth2\Client\Provider\Exception\IdentityProviderException
**/
Expand All @@ -276,7 +362,7 @@ public function testExceptionThrownWhenErrorObjectReceived()
$message = uniqid();
$status = rand(400,600);
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')->andReturn('{"error_description": "'.$message.'","error": "invalid_request"}');
$postResponse->shouldReceive('getBody')->andReturn('{"message": "'.$message.'","status": '.$status.', "serviceErrorCode": 100}');
$postResponse->shouldReceive('getHeader')->andReturn(['content-type' => 'json']);
$postResponse->shouldReceive('getStatusCode')->andReturn($status);

Expand Down

0 comments on commit 9e9ea27

Please sign in to comment.