Skip to content

Commit b926c66

Browse files
authored
Merge pull request #2039 from d9beuD/master
Add new LinkedIn OpenID resource
2 parents b802eab + f356455 commit b926c66

File tree

5 files changed

+259
-1
lines changed

5 files changed

+259
-1
lines changed

docs/2-configuring_resource_owners.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,8 @@ hwi_oauth:
6060
- [itembase](resource_owners/itembase.md)
6161
- [Jira](resource_owners/jira.md)
6262
- [Keycloak](resource_owners/keycloak.md)
63-
- [Linkedin](resource_owners/linkedin.md)
63+
- [LinkedIn OpenID](resource_owners/linkedin_openid.md)
64+
- [LinkedIn](resource_owners/linkedin.md)
6465
- [Mail.ru](resource_owners/mailru.md)
6566
- [Odnoklassniki](resource_owners/odnoklassniki.md)
6667
- [Passage](resource_owners/passage.md)
+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
Step 2x: Setup Linkedin OpenID
2+
=======================
3+
First you will have to register your application on Linkedin. Check out the
4+
documentation for more information: https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2.
5+
6+
Next configure a resource owner of type `linkedin_openid` with appropriate `client_id`,
7+
`client_secret` and `scope`.
8+
Example of values for scope: `openid`, `profile`, `email`
9+
as described by [Authenticating Members](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2?source=recommendations#authenticating-members)
10+
11+
```yaml
12+
# config/packages/hwi_oauth.yaml
13+
14+
hwi_oauth:
15+
resource_owners:
16+
any_name:
17+
type: linkedin_openid
18+
client_id: <client_id>
19+
client_secret: <client_secret>
20+
scope: <scope>
21+
```
22+
23+
When you're done. Continue by configuring the security layer or go back to
24+
setup more resource owners.
25+
26+
- [Step 2: Configuring resource owners (Facebook, GitHub, Google, Windows Live and others](../2-configuring_resource_owners.md)
27+
- [Step 3: Configuring the security layer](../3-configuring_the_security_layer.md).
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the HWIOAuthBundle package.
5+
*
6+
* (c) Hardware Info <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace HWI\Bundle\OAuthBundle\OAuth\ResourceOwner;
13+
14+
use HWI\Bundle\OAuthBundle\OAuth\Response\LinkedinOpenIdUserResponse;
15+
use Symfony\Component\OptionsResolver\OptionsResolver;
16+
17+
/**
18+
* @author Francisco Facioni <[email protected]>
19+
* @author Joseph Bielawski <[email protected]>
20+
*/
21+
final class LinkedinOpenIdResourceOwner extends GenericOAuth2ResourceOwner
22+
{
23+
public const TYPE = 'linkedin_openid';
24+
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
protected array $paths = [
29+
'identifier' => 'sub',
30+
'nickname' => 'email',
31+
'firstname' => 'given_name',
32+
'lastname' => 'family_name',
33+
'email' => 'email',
34+
'profilepicture' => 'picture',
35+
];
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function getUserInformation(array $accessToken, array $extraParameters = [])
41+
{
42+
return parent::getUserInformation($accessToken, $extraParameters);
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
protected function doGetTokenRequest($url, array $parameters = [])
49+
{
50+
$parameters['client_id'] = $this->options['client_id'];
51+
$parameters['client_secret'] = $this->options['client_secret'];
52+
53+
return $this->httpRequest($this->normalizeUrl($url, $parameters), null, [], 'POST');
54+
}
55+
56+
/**
57+
* {@inheritdoc}
58+
*/
59+
protected function httpRequest($url, $content = null, array $headers = [], $method = null)
60+
{
61+
// LinkedIn v2 API is supposed to require Content-Type: application/json but it works without
62+
// and request to get the access token doesn't seems to work with Content-Type: application/json
63+
// so we don't put any Content-Type header.
64+
// Skip the Content-Type header in GenericOAuth2ResourceOwner::httpRequest
65+
//
66+
// LinkedIn API requires to always set Content-Length in POST requests
67+
if ('POST' === $method) {
68+
$headers['Content-Length'] = \is_string($content) ? (string) \strlen($content) : '0';
69+
}
70+
71+
return AbstractResourceOwner::httpRequest($url, $content, $headers, $method);
72+
}
73+
74+
/**
75+
* {@inheritdoc}
76+
*/
77+
protected function configureOptions(OptionsResolver $resolver)
78+
{
79+
parent::configureOptions($resolver);
80+
81+
$resolver->setDefaults([
82+
'scope' => 'openid profile email',
83+
'authorization_url' => 'https://www.linkedin.com/oauth/v2/authorization',
84+
'access_token_url' => 'https://www.linkedin.com/oauth/v2/accessToken',
85+
'infos_url' => 'https://api.linkedin.com/v2/userinfo',
86+
87+
'user_response_class' => LinkedinOpenIdUserResponse::class,
88+
89+
'csrf' => true,
90+
91+
'use_bearer_authorization' => true,
92+
]);
93+
}
94+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the HWIOAuthBundle package.
5+
*
6+
* (c) Hardware Info <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace HWI\Bundle\OAuthBundle\OAuth\Response;
13+
14+
final class LinkedinOpenIdUserResponse extends PathUserResponse
15+
{
16+
/**
17+
* {@inheritdoc}
18+
*/
19+
public function getFirstName(): ?string
20+
{
21+
return $this->getValueForPath('firstname');
22+
}
23+
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function getLastName(): ?string
28+
{
29+
return $this->getValueForPath('lastname');
30+
}
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function getProfilePicture(): ?string
36+
{
37+
return $this->getValueForPath('profilepicture');
38+
}
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the HWIOAuthBundle package.
5+
*
6+
* (c) Hardware Info <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace HWI\Bundle\OAuthBundle\Tests\OAuth\ResourceOwner;
13+
14+
use HWI\Bundle\OAuthBundle\OAuth\ResourceOwner\LinkedinOpenIdResourceOwner;
15+
use HWI\Bundle\OAuthBundle\OAuth\Response\AbstractUserResponse;
16+
use HWI\Bundle\OAuthBundle\Test\Fixtures\CustomUserResponse;
17+
use HWI\Bundle\OAuthBundle\Test\OAuth\ResourceOwner\GenericOAuth2ResourceOwnerTestCase;
18+
19+
final class LinkedinOpenIdResourceOwnerTest extends GenericOAuth2ResourceOwnerTestCase
20+
{
21+
protected string $resourceOwnerClass = LinkedinOpenIdResourceOwner::class;
22+
protected string $userResponse = <<<json
23+
{
24+
"sub": "CM9X5BxxK8",
25+
"email_verified": true,
26+
"name": "John Smith",
27+
"locale": {
28+
"country": "US",
29+
"language": "en"
30+
},
31+
"given_name": "John",
32+
"family_name": "Smith",
33+
"email": "[email protected]",
34+
"picture": "https://website.com/picture.jpg"
35+
}
36+
json;
37+
protected array $paths = [
38+
'identifier' => 'sub',
39+
'nickname' => 'email',
40+
'firstname' => 'given_name',
41+
'lastname' => 'family_name',
42+
'email' => 'email',
43+
'profilepicture' => 'picture',
44+
];
45+
protected bool $csrf = true;
46+
47+
protected string $authorizationUrlBasePart = 'http://user.auth/?test=2&response_type=code&client_id=clientid&scope=openid+profile+email';
48+
49+
protected int $httpClientCalls = 1;
50+
51+
public function testCustomResponseClass(): void
52+
{
53+
$class = CustomUserResponse::class;
54+
55+
$resourceOwner = $this->createResourceOwner(
56+
['user_response_class' => $class],
57+
[],
58+
[
59+
$this->createMockResponse($this->userResponse, 'application/json; charset=utf-8'),
60+
$this->createMockResponse($this->userResponse, 'application/json; charset=utf-8'),
61+
]
62+
);
63+
64+
/** @var CustomUserResponse */
65+
$userResponse = $resourceOwner->getUserInformation($this->tokenData);
66+
67+
$this->assertInstanceOf($class, $userResponse);
68+
$this->assertEquals('foo666', $userResponse->getUserIdentifier());
69+
$this->assertEquals('foo', $userResponse->getNickname());
70+
$this->assertEquals('token', $userResponse->getAccessToken());
71+
$this->assertNull($userResponse->getRefreshToken());
72+
$this->assertNull($userResponse->getExpiresIn());
73+
}
74+
75+
public function testGetUserInformation(): void
76+
{
77+
$resourceOwner = $this->createResourceOwner(
78+
[],
79+
[],
80+
[
81+
$this->createMockResponse($this->userResponse, 'application/json; charset=utf-8'),
82+
$this->createMockResponse($this->userResponse, 'application/json; charset=utf-8'),
83+
]
84+
);
85+
86+
/** @var AbstractUserResponse $userResponse */
87+
$userResponse = $resourceOwner->getUserInformation($this->tokenData);
88+
89+
$this->assertEquals('CM9X5BxxK8', $userResponse->getUserIdentifier());
90+
$this->assertEquals('[email protected]', $userResponse->getNickname());
91+
$this->assertEquals('John', $userResponse->getFirstName());
92+
$this->assertEquals('Smith', $userResponse->getLastName());
93+
$this->assertEquals('token', $userResponse->getAccessToken());
94+
$this->assertNull($userResponse->getRefreshToken());
95+
$this->assertNull($userResponse->getExpiresIn());
96+
}
97+
}

0 commit comments

Comments
 (0)