Skip to content

Commit 5e4e9a6

Browse files
authored
Merge pull request #81 from CakeDC/feature/openid-connect-linkedin
Implement LinkedIn via OpenID-Connect
2 parents e104e5d + 891a7b7 commit 5e4e9a6

File tree

9 files changed

+569
-30
lines changed

9 files changed

+569
-30
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
fail-fast: false
1313
matrix:
14-
php-version: ['7.4', '8.0', '8.1']
14+
php-version: ['7.4', '8.0', '8.1', '8.2']
1515
db-type: [sqlite, mysql, pgsql]
1616
prefer-lowest: ['']
1717

@@ -24,7 +24,7 @@ jobs:
2424
if: matrix.db-type == 'pgsql'
2525
run: docker run --rm --name=postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=cakephp -p 5432:5432 -d postgres
2626

27-
- uses: actions/checkout@v2
27+
- uses: actions/checkout@v4
2828

2929
- name: Setup PHP
3030
uses: shivammathur/setup-php@v2
@@ -43,7 +43,7 @@ jobs:
4343
run: echo "::set-output name=date::$(date +'%Y-%m')"
4444

4545
- name: Cache composer dependencies
46-
uses: actions/cache@v1
46+
uses: actions/cache@v3
4747
with:
4848
path: ${{ steps.composer-cache.outputs.dir }}
4949
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}
@@ -57,30 +57,30 @@ jobs:
5757
fi
5858
5959
- name: Setup problem matchers for PHPUnit
60-
if: matrix.php-version == '7.4' && matrix.db-type == 'mysql'
60+
if: matrix.php-version == '8.1' && matrix.db-type == 'mysql'
6161
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
6262

6363
- name: Run PHPUnit
6464
run: |
6565
if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi
6666
if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:root@127.0.0.1/cakephp'; fi
6767
if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:postgres@127.0.0.1/postgres'; fi
68-
if [[ ${{ matrix.php-version }} == '7.4' ]]; then
69-
export CODECOVERAGE=1 && vendor/bin/phpunit --verbose --coverage-clover=coverage.xml
68+
if [[ ${{ matrix.php-version }} == '8.1' ]]; then
69+
export CODECOVERAGE=1 && vendor/bin/phpunit --stderr --verbose --coverage-clover=coverage.xml
7070
else
71-
vendor/bin/phpunit
71+
vendor/bin/phpunit --stderr
7272
fi
7373
7474
- name: Submit code coverage
75-
if: matrix.php-version == '7.4'
76-
uses: codecov/codecov-action@v1
75+
if: matrix.php-version == '8.1'
76+
uses: codecov/codecov-action@v3
7777

7878
cs-stan:
7979
name: Coding Standard & Static Analysis
8080
runs-on: ubuntu-22.04
8181

8282
steps:
83-
- uses: actions/checkout@v2
83+
- uses: actions/checkout@v4
8484

8585
- name: Setup PHP
8686
uses: shivammathur/setup-php@v2
@@ -99,7 +99,7 @@ jobs:
9999
run: echo "::set-output name=date::$(date +'%Y-%m')"
100100

101101
- name: Cache composer dependencies
102-
uses: actions/cache@v1
102+
uses: actions/cache@v3
103103
with:
104104
path: ${{ steps.composer-cache.outputs.dir }}
105105
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
/.idea
44
composer.lock
55
.php_cs.cache
6+
.phpunit.result.cache

Docs/Documentation/Social.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Social Layer
22
============
33
The social layer provide a easier way to handle social provider authentication
4-
with provides using OAuth1 or OAuth2. The idea is to provide a base
4+
with provides using OAuth1 or OAuth2. The idea is to provide a base
55
interface for both OAuth and OAuth2.
66

77
***Make sure to load the bootstap.php file of this plugin!***
@@ -12,9 +12,10 @@ We have mappers to allow you a quick start with these providers:
1212
- Facebook
1313
- Google
1414
- Instagram
15-
- LinkedIn
15+
- LinkedIn (Deprecated, they switched to OpenID-Connect)
16+
- LinkedInOpenIDConnect (New, OIDC based authentication)
1617
- Pinterest
17-
- Tumblr
18+
- Tumblr
1819
- Twitter
1920

2021
You must define 'options.redirectUri', 'options.clientId' and
@@ -57,7 +58,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory;
5758
->getAuthorizationUrl($this->request)
5859
);
5960
}
60-
61+
6162
/**
6263
* Callback to get user information from provider
6364
*
@@ -80,7 +81,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory;
8081
}
8182
$data = $server->getUser($this->request);
8283
$data = (new MapUser())($server, $data);
83-
84+
8485
//your code
8586
} catch (\Exception $e) {
8687
$this->log($log);
@@ -92,4 +93,4 @@ Working with cakephp/authentication
9293
If you're using the new cakephp/authentication we recommend you to use
9394
the SocialAuthenticator and SocialMiddleware provided in this plugin. For more
9495
details of how to handle social authentication with cakephp/authentication, please check
95-
how we implemented at CakeDC/Users plugins.
96+
how we implemented at CakeDC/Users plugins.

composer.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
},
2828
"-minimum-stability": "dev",
2929
"require": {
30-
"php": ">=7.2.0",
30+
"php": ">=7.4.0",
3131
"cakephp/cakephp": "^4.3"
3232
},
3333
"require-dev": {
@@ -44,7 +44,8 @@
4444
"cakephp/cakephp-codesniffer": "^4.0",
4545
"cakephp/authentication": "^2.0",
4646
"yubico/u2flib-server": "^1.0",
47-
"php-coveralls/php-coveralls": "^2.4"
47+
"php-coveralls/php-coveralls": "^2.4",
48+
"firebase/php-jwt": "^v6.8"
4849
},
4950
"suggest": {
5051
},

config/auth.php

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*/
1111

1212
use Cake\Routing\Router;
13+
1314
return [
1415
'OAuth.path' => ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialLogin', 'prefix' => null],
1516
'OAuth.providers' => [
@@ -23,7 +24,7 @@
2324
'redirectUri' => Router::fullBaseUrl() . '/auth/facebook',
2425
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/facebook',
2526
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/facebook',
26-
]
27+
],
2728
],
2829
'twitter' => [
2930
'service' => 'CakeDC\Auth\Social\Service\OAuth1Service',
@@ -33,8 +34,9 @@
3334
'redirectUri' => Router::fullBaseUrl() . '/auth/twitter',
3435
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/twitter',
3536
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/twitter',
36-
]
37+
],
3738
],
39+
// Deprecated, LinkedIn switched to OpenID-Connect and OAuth2 is no longer working properly
3840
'linkedIn' => [
3941
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
4042
'className' => 'League\OAuth2\Client\Provider\LinkedIn',
@@ -43,7 +45,18 @@
4345
'redirectUri' => Router::fullBaseUrl() . '/auth/linkedIn',
4446
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedIn',
4547
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedIn',
46-
]
48+
],
49+
],
50+
'linkedInOpenIDConnect' => [
51+
'service' => 'CakeDC\Auth\Social\Service\OpenIDConnectService',
52+
'className' => 'League\OAuth2\Client\Provider\LinkedIn',
53+
'mapper' => 'CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect',
54+
'options' => [
55+
'redirectUri' => Router::fullBaseUrl() . '/auth/linkedInOpenIDConnect',
56+
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedInOpenIDConnect',
57+
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedInOpenIDConnect',
58+
'defaultScopes' => ['email', 'openid', 'profile'],
59+
],
4760
],
4861
'instagram' => [
4962
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
@@ -53,7 +66,7 @@
5366
'redirectUri' => Router::fullBaseUrl() . '/auth/instagram',
5467
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/instagram',
5568
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/instagram',
56-
]
69+
],
5770
],
5871
'google' => [
5972
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
@@ -64,7 +77,7 @@
6477
'redirectUri' => Router::fullBaseUrl() . '/auth/google',
6578
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/google',
6679
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/google',
67-
]
80+
],
6881
],
6982
'amazon' => [
7083
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
@@ -74,7 +87,7 @@
7487
'redirectUri' => Router::fullBaseUrl() . '/auth/amazon',
7588
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/amazon',
7689
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/amazon',
77-
]
90+
],
7891
],
7992
'azure' => [
8093
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
@@ -84,7 +97,7 @@
8497
'redirectUri' => Router::fullBaseUrl() . '/auth/azure',
8598
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/azure',
8699
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/azure',
87-
]
100+
],
88101
],
89102
],
90103
'TwoFactorProcessors' => [
@@ -111,7 +124,7 @@
111124
// QR-code provider (more on this later)
112125
'qrcodeprovider' => null,
113126
// Random Number Generator provider (more on this later)
114-
'rngprovider' => null
127+
'rngprovider' => null,
115128
],
116129
'U2f' => [
117130
'enabled' => false,
@@ -121,7 +134,7 @@
121134
'controller' => 'Users',
122135
'action' => 'u2f',
123136
'prefix' => false,
124-
]
137+
],
125138
],
126139
'Webauthn2fa' => [
127140
'enabled' => false,
@@ -133,6 +146,6 @@
133146
'controller' => 'Users',
134147
'action' => 'webauthn2fa',
135148
'prefix' => false,
136-
]
137-
]
149+
],
150+
],
138151
];
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
/**
5+
* Copyright 2010 - 2019, Cake Development Corporation (https://www.cakedc.com)
6+
*
7+
* Licensed under The MIT License
8+
* Redistributions of files must retain the above copyright notice.
9+
*
10+
* @copyright Copyright 2010 - 2019, Cake Development Corporation (https://www.cakedc.com)
11+
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
12+
*/
13+
14+
namespace CakeDC\Auth\Social\Mapper;
15+
16+
class LinkedInOpenIDConnect extends AbstractMapper
17+
{
18+
/**
19+
* Map for provider fields
20+
*
21+
* @var array
22+
*/
23+
protected $_mapFields = [
24+
'avatar' => 'picture',
25+
'first_name' => 'given_name',
26+
'last_name' => 'family_name',
27+
'email' => 'email',
28+
'link' => 'link',
29+
'id' => 'sub',
30+
];
31+
32+
protected function _link(): string
33+
{
34+
// no way to retrieve the public url from the users profile
35+
36+
return 'https://www.linkedin.com';
37+
}
38+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace CakeDC\Auth\Social\Service;
5+
6+
use Cake\Http\Client;
7+
use Cake\Http\Exception\BadRequestException;
8+
use Cake\Http\ServerRequest;
9+
use Firebase\JWT\JWK;
10+
use Firebase\JWT\JWT;
11+
use Psr\Http\Message\ServerRequestInterface;
12+
13+
class OpenIDConnectService extends OAuth2Service
14+
{
15+
protected $_defaultConfig = [
16+
'openid' => [
17+
'baseUrl' => 'https://www.linkedin.com/',
18+
'url' => 'https://www.linkedin.com/oauth/.well-known/openid-configuration',
19+
'jwk' => [
20+
'defaultAlgorithm' => 'RS256',
21+
],
22+
],
23+
];
24+
25+
public function getUser(ServerRequestInterface $request): array
26+
{
27+
if (!$request instanceof ServerRequest) {
28+
throw new \BadMethodCallException('Request must be an instance of ServerRequest');
29+
}
30+
if (!$this->validate($request)) {
31+
throw new BadRequestException('Invalid OAuth2 state');
32+
}
33+
34+
$code = $request->getQuery('code');
35+
/** @var \League\OAuth2\Client\Token\AccessToken $token */
36+
$token = $this->provider->getAccessToken('authorization_code', ['code' => $code]);
37+
$tokenValues = $token->getValues();
38+
$idToken = $tokenValues['id_token'] ?? null;
39+
if (!$idToken) {
40+
throw new BadRequestException('Missing id_token in response');
41+
}
42+
try {
43+
$idTokenDecoded = JWT::decode($idToken, $this->getIdTokenKeys());
44+
45+
return ['token' => $token] + (array)$idTokenDecoded;
46+
} catch (\Exception $ex) {
47+
throw new BadRequestException('Invalid id token. ' . $ex->getMessage());
48+
}
49+
}
50+
51+
protected function getIdTokenKeys(): array
52+
{
53+
$discoverData = $this->discover();
54+
$jwksUri = $discoverData['jwks_uri'] ?? null;
55+
if (!$jwksUri) {
56+
throw new BadRequestException(
57+
'No `jwks_uri` in discover data. Unable to retrieve the JWT signature public key'
58+
);
59+
}
60+
if (strpos($jwksUri, $this->getConfig('openid.baseUrl')) !== 0) {
61+
throw new BadRequestException(
62+
'Invalid `jwks_uri` in discover data. It is not pointing to ' .
63+
$this->getConfig('openid.baseUrl')
64+
);
65+
}
66+
$client = $this->getHttpClient();
67+
$jwksData = $client->get($jwksUri)->getJson();
68+
if (!$jwksData) {
69+
throw new BadRequestException(
70+
'Unable to retrieve jwks. Not found in the `jwks_uri` contents'
71+
);
72+
}
73+
74+
return JWK::parseKeySet($jwksData, $this->getConfig('openid.jwk.defaultAlgorithm'));
75+
}
76+
77+
public function discover(): array
78+
{
79+
$openidUrl = $this->getConfig('openid.url');
80+
$client = $this->getHttpClient();
81+
82+
return $client->get($openidUrl)->getJson();
83+
}
84+
85+
protected function getHttpClient(): Client
86+
{
87+
return new Client();
88+
}
89+
}

0 commit comments

Comments
 (0)