Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement LinkedIn via OpenID-Connect #81

Open
wants to merge 13 commits into
base: 7.next-cake4
Choose a base branch
from
28 changes: 14 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ on:

jobs:
testsuite:
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
php-version: ['7.2', '7.3', '7.4', '8.0', '8.1']
php-version: ['7.4', '8.0', '8.1', '8.2']
db-type: [sqlite, mysql, pgsql]
prefer-lowest: ['']

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

- uses: actions/checkout@v2
- uses: actions/checkout@v4

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

- name: Cache composer dependencies
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}
Expand All @@ -57,35 +57,35 @@ jobs:
fi

- name: Setup problem matchers for PHPUnit
if: matrix.php-version == '7.4' && matrix.db-type == 'mysql'
if: matrix.php-version == '8.1' && matrix.db-type == 'mysql'
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

- name: Run PHPUnit
run: |
if [[ ${{ matrix.db-type }} == 'sqlite' ]]; then export DB_URL='sqlite:///:memory:'; fi
if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export DB_URL='mysql://root:[email protected]/cakephp'; fi
if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export DB_URL='postgres://postgres:[email protected]/postgres'; fi
if [[ ${{ matrix.php-version }} == '7.4' ]]; then
export CODECOVERAGE=1 && vendor/bin/phpunit --verbose --coverage-clover=coverage.xml
if [[ ${{ matrix.php-version }} == '8.1' ]]; then
export CODECOVERAGE=1 && vendor/bin/phpunit --stderr --verbose --coverage-clover=coverage.xml
else
vendor/bin/phpunit
vendor/bin/phpunit --stderr
fi

- name: Submit code coverage
if: matrix.php-version == '7.4'
uses: codecov/codecov-action@v1
if: matrix.php-version == '8.1'
uses: codecov/codecov-action@v3

cs-stan:
name: Coding Standard & Static Analysis
runs-on: ubuntu-18.04
runs-on: ubuntu-22.04

steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4

- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '7.4'
php-version: '8.1'
extensions: mbstring, intl, apcu, memcached, redis
tools: cs2pr
coverage: none
Expand All @@ -99,7 +99,7 @@ jobs:
run: echo "::set-output name=date::$(date +'%Y-%m')"

- name: Cache composer dependencies
uses: actions/cache@v1
uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ steps.key-date.outputs.date }}-${{ hashFiles('composer.json') }}-${{ matrix.prefer-lowest }}
Expand Down
13 changes: 7 additions & 6 deletions Docs/Documentation/Social.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Social Layer
============
The social layer provide a easier way to handle social provider authentication
with provides using OAuth1 or OAuth2. The idea is to provide a base
with provides using OAuth1 or OAuth2. The idea is to provide a base
interface for both OAuth and OAuth2.

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

You must define 'options.redirectUri', 'options.clientId' and
Expand Down Expand Up @@ -57,7 +58,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory;
->getAuthorizationUrl($this->request)
);
}

/**
* Callback to get user information from provider
*
Expand All @@ -80,7 +81,7 @@ use CakeDC\Auth\Social\Service\ServiceFactory;
}
$data = $server->getUser($this->request);
$data = (new MapUser())($server, $data);

//your code
} catch (\Exception $e) {
$this->log($log);
Expand All @@ -92,4 +93,4 @@ Working with cakephp/authentication
If you're using the new cakephp/authentication we recommend you to use
the SocialAuthenticator and SocialMiddleware provided in this plugin. For more
details of how to handle social authentication with cakephp/authentication, please check
how we implemented at CakeDC/Users plugins.
how we implemented at CakeDC/Users plugins.
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"-minimum-stability": "dev",
"require": {
"php": ">=7.2.0",
"php": ">=7.4.0",
"cakephp/cakephp": "^4.3"
},
"require-dev": {
Expand All @@ -44,7 +44,8 @@
"cakephp/cakephp-codesniffer": "^4.0",
"cakephp/authentication": "^2.0",
"yubico/u2flib-server": "^1.0",
"php-coveralls/php-coveralls": "^2.4"
"php-coveralls/php-coveralls": "^2.4",
"firebase/php-jwt": "^v6.8"
},
"suggest": {
},
Expand Down
35 changes: 24 additions & 11 deletions config/auth.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

use Cake\Routing\Router;

return [
'OAuth.path' => ['plugin' => 'CakeDC/Users', 'controller' => 'Users', 'action' => 'socialLogin', 'prefix' => null],
'OAuth.providers' => [
Expand All @@ -23,7 +24,7 @@
'redirectUri' => Router::fullBaseUrl() . '/auth/facebook',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/facebook',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/facebook',
]
],
],
'twitter' => [
'service' => 'CakeDC\Auth\Social\Service\OAuth1Service',
Expand All @@ -33,8 +34,9 @@
'redirectUri' => Router::fullBaseUrl() . '/auth/twitter',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/twitter',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/twitter',
]
],
],
// Deprecated, LinkedIn switched to OpenID-Connect and OAuth2 is no longer working properly
'linkedIn' => [
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
'className' => 'League\OAuth2\Client\Provider\LinkedIn',
Expand All @@ -43,7 +45,18 @@
'redirectUri' => Router::fullBaseUrl() . '/auth/linkedIn',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedIn',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedIn',
]
],
],
'linkedInOpenIDConnect' => [
'service' => 'CakeDC\Auth\Social\Service\OpenIDConnectService',
'className' => 'League\OAuth2\Client\Provider\LinkedIn',
'mapper' => 'CakeDC\Auth\Social\Mapper\LinkedInOpenIDConnect',
'options' => [
'redirectUri' => Router::fullBaseUrl() . '/auth/linkedInOpenIDConnect',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/linkedInOpenIDConnect',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/linkedInOpenIDConnect',
'defaultScopes' => ['email', 'openid', 'profile'],
],
],
'instagram' => [
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
Expand All @@ -53,7 +66,7 @@
'redirectUri' => Router::fullBaseUrl() . '/auth/instagram',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/instagram',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/instagram',
]
],
],
'google' => [
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
Expand All @@ -64,7 +77,7 @@
'redirectUri' => Router::fullBaseUrl() . '/auth/google',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/google',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/google',
]
],
],
'amazon' => [
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
Expand All @@ -74,7 +87,7 @@
'redirectUri' => Router::fullBaseUrl() . '/auth/amazon',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/amazon',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/amazon',
]
],
],
'azure' => [
'service' => 'CakeDC\Auth\Social\Service\OAuth2Service',
Expand All @@ -84,7 +97,7 @@
'redirectUri' => Router::fullBaseUrl() . '/auth/azure',
'linkSocialUri' => Router::fullBaseUrl() . '/link-social/azure',
'callbackLinkSocialUri' => Router::fullBaseUrl() . '/callback-link-social/azure',
]
],
],
],
'OneTimePasswordAuthenticator' => [
Expand All @@ -106,7 +119,7 @@
// QR-code provider (more on this later)
'qrcodeprovider' => null,
// Random Number Generator provider (more on this later)
'rngprovider' => null
'rngprovider' => null,
],
'U2f' => [
'enabled' => false,
Expand All @@ -116,7 +129,7 @@
'controller' => 'Users',
'action' => 'u2f',
'prefix' => false,
]
],
],
'Webauthn2fa' => [
'enabled' => false,
Expand All @@ -128,6 +141,6 @@
'controller' => 'Users',
'action' => 'webauthn2fa',
'prefix' => false,
]
]
],
],
];
38 changes: 38 additions & 0 deletions src/Social/Mapper/LinkedInOpenIDConnect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);

/**
* Copyright 2010 - 2019, Cake Development Corporation (https://www.cakedc.com)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2010 - 2019, Cake Development Corporation (https://www.cakedc.com)
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/

namespace CakeDC\Auth\Social\Mapper;

class LinkedInOpenIDConnect extends AbstractMapper
{
/**
* Map for provider fields
*
* @var array
*/
protected $_mapFields = [
'avatar' => 'picture',
'first_name' => 'given_name',
'last_name' => 'family_name',
'email' => 'email',
'link' => 'link',
'id' => 'sub',
];

protected function _link(): string
{
// no way to retrieve the public url from the users profile

return 'https://www.linkedin.com';
}
}
89 changes: 89 additions & 0 deletions src/Social/Service/OpenIDConnectService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);

namespace CakeDC\Auth\Social\Service;

use Cake\Http\Client;
use Cake\Http\Exception\BadRequestException;
use Cake\Http\ServerRequest;
use Firebase\JWT\JWK;
use Firebase\JWT\JWT;
use Psr\Http\Message\ServerRequestInterface;

class OpenIDConnectService extends OAuth2Service
{
protected $_defaultConfig = [
'openid' => [
'baseUrl' => 'https://www.linkedin.com/',
'url' => 'https://www.linkedin.com/oauth/.well-known/openid-configuration',
'jwk' => [
'defaultAlgorithm' => 'RS256',
],
],
];

public function getUser(ServerRequestInterface $request): array
{
if (!$request instanceof ServerRequest) {
throw new \BadMethodCallException('Request must be an instance of ServerRequest');
}
if (!$this->validate($request)) {
throw new BadRequestException('Invalid OAuth2 state');
}

$code = $request->getQuery('code');
/** @var \League\OAuth2\Client\Token\AccessToken $token */
$token = $this->provider->getAccessToken('authorization_code', ['code' => $code]);
$tokenValues = $token->getValues();
$idToken = $tokenValues['id_token'] ?? null;
if (!$idToken) {
throw new BadRequestException('Missing id_token in response');
}
try {
$idTokenDecoded = JWT::decode($idToken, $this->getIdTokenKeys());

return ['token' => $token] + (array)$idTokenDecoded;
} catch (\Exception $ex) {
throw new BadRequestException('Invalid id token. ' . $ex->getMessage());
}
}

protected function getIdTokenKeys(): array
{
$discoverData = $this->discover();
$jwksUri = $discoverData['jwks_uri'] ?? null;
if (!$jwksUri) {
throw new BadRequestException(
'No `jwks_uri` in discover data. Unable to retrieve the JWT signature public key'
);
}
if (strpos($jwksUri, $this->getConfig('openid.baseUrl')) !== 0) {
throw new BadRequestException(
'Invalid `jwks_uri` in discover data. It is not pointing to ' .
$this->getConfig('openid.baseUrl')
);
}
$client = $this->getHttpClient();
$jwksData = $client->get($jwksUri)->getJson();
if (!$jwksData) {
throw new BadRequestException(
'Unable to retrieve jwks. Not found in the `jwks_uri` contents'
);
}

return JWK::parseKeySet($jwksData, $this->getConfig('openid.jwk.defaultAlgorithm'));
}

public function discover(): array
{
$openidUrl = $this->getConfig('openid.url');
$client = $this->getHttpClient();

return $client->get($openidUrl)->getJson();
}

protected function getHttpClient(): Client
{
return new Client();
}
}
Loading
Loading