Skip to content

Commit

Permalink
keycloak openidconnect provider added
Browse files Browse the repository at this point in the history
  • Loading branch information
gguseynov authored and ovr committed Sep 19, 2020
1 parent 785bf64 commit 03aecc5
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 0 deletions.
11 changes: 11 additions & 0 deletions example/config.php.dist
Expand Up @@ -234,5 +234,16 @@ return [
'read'
]
],
// keycloak
'keycloak' => [
// 'name' => 'abc', // override for multiple providers based on keycloak
'baseUrl' => 'https://keycloak_server/auth',
'realm' => 'your_master',
'applicationId' => 'your_client',
'applicationSecret' => 'your_client_uuid4_secret',
'scope' => [
'email', 'profile' // openid will be always added
],
],
]
];
1 change: 1 addition & 0 deletions src/Auth/CollectionFactory.php
Expand Up @@ -55,6 +55,7 @@ class CollectionFactory implements FactoryInterface
// OpenIDConnect
OpenIDConnect\Provider\Apple::NAME => OpenIDConnect\Provider\Apple::class,
OpenIDConnect\Provider\Google::NAME => OpenIDConnect\Provider\Google::class,
OpenIDConnect\Provider\Keycloak::NAME => OpenIDConnect\Provider\Keycloak::class,
OpenIDConnect\Provider\PixelPin::NAME => OpenIDConnect\Provider\PixelPin::class,
];

Expand Down
148 changes: 148 additions & 0 deletions src/OpenIDConnect/Provider/Keycloak.php
@@ -0,0 +1,148 @@
<?php

declare(strict_types=1);

namespace SocialConnect\OpenIDConnect\Provider;

use SocialConnect\Common\ArrayHydrator;
use SocialConnect\Common\Entity\User;
use SocialConnect\Common\Exception\InvalidArgumentException;
use SocialConnect\Common\HttpStack;
use SocialConnect\OpenIDConnect\AbstractProvider;
use SocialConnect\OpenIDConnect\AccessToken;
use SocialConnect\Provider\AccessTokenInterface;
use SocialConnect\Provider\Session\SessionInterface;

class Keycloak extends AbstractProvider
{
const NAME = 'keycloak';

protected $name;

protected $baseUrl;

protected $realm;

protected $protocol = 'openid-connect';

protected $hydrateMapper = [];

public function __construct(HttpStack $httpStack, SessionInterface $session, array $parameters)
{
parent::__construct($httpStack, $session, $parameters);

$this->name = isset($parameters['name']) ? $parameters['name'] : self::NAME;

$this->baseUrl = rtrim($this->getRequiredStringParameter('baseUrl', $parameters), '/') . '/';

$this->realm = $this->getRequiredStringParameter('realm', $parameters);

if (isset($parameters['protocol'])) {
$this->protocol = $parameters['protocol'];
}

if (isset($parameters['hydrateMapper'])) {
$this->hydrateMapper = $parameters['hydrateMapper'];
}
}

public function getBaseUri()
{
return $this->baseUrl;
}

public function getName()
{
return $this->name;
}

public function prepareRequest(string $method, string $uri, array &$headers, array &$query, AccessTokenInterface $accessToken = null): void
{
if ($accessToken) {
$headers['Authorization'] = 'Bearer ' . $accessToken->getToken();
}
}

public function getIdentity(AccessTokenInterface $accessToken)
{
$url = sprintf('realms/%s/protocol/%s/userinfo', $this->realm, $this->protocol);

$response = $this->request('GET', $url, [], $accessToken, []);

$hydrator = new ArrayHydrator($this->hydrateMapper + [
'sub' => 'id',
'preferred_username' => 'username',
'given_name' => 'firstname',
'family_name' => 'lastname',
'email' => 'email',
'email_verified' => 'emailVerified',
'name' => 'fullname',
'gender' => static function ($value, User $user) {
$user->setSex($value);
},
'birthdate' => static function ($value, User $user) {
$user->setBirthday(date_create($value, new \DateTimeZone('UTC')));
},
]);

return $hydrator->hydrate(new User(), $response);
}

public function getAuthorizeUri()
{
return $this->getBaseUri() . sprintf('realms/%s/protocol/%s/auth', $this->realm, $this->protocol);
}

public function getRequestTokenUri()
{
return $this->getBaseUri() . sprintf('realms/%s/protocol/%s/token', $this->realm, $this->protocol);
}

public function getOpenIdUrl()
{
return $this->getBaseUri() . sprintf('realms/%s/.well-known/openid-configuration', $this->realm);
}

public function extractIdentity(AccessTokenInterface $accessToken)
{
if (!$accessToken instanceof AccessToken) {
throw new InvalidArgumentException(
'$accessToken must be instance AccessToken'
);
}

$jwt = $accessToken->getJwt();

$hydrator = new ArrayHydrator($this->hydrateMapper + [
'sub' => 'id',
'preferred_username' => 'username',
'given_name' => 'firstname',
'family_name' => 'lastname',
'email' => 'email',
'email_verified' => 'emailVerified',
'name' => 'fullname',
'gender' => static function ($value, User $user) {
$user->setSex($value);
},
'birthdate' => static function ($value, User $user) {
$user->setBirthday(date_create($value, new \DateTimeZone('UTC')));
},
]);

/** @var User $user */
$user = $hydrator->hydrate(new User(), $jwt->getPayload());

return $user;
}

public function getScopeInline()
{
$scopes = $this->scope;

if (!in_array('openid', $scopes)) {
array_unshift($scopes, 'openid');
}

return implode(' ', $scopes);
}
}
33 changes: 33 additions & 0 deletions tests/Test/OpenIDConnect/Provider/KeycloakTest.php
@@ -0,0 +1,33 @@
<?php

namespace Test\OpenIDConnect\Provider;

use Psr\Http\Message\ResponseInterface;

class KeycloakTest extends AbstractProviderTestCase
{
/**
* {@inheritdoc}
*/
protected function getProviderClassName()
{
return \SocialConnect\OpenIDConnect\Provider\Keycloak::class;
}

public function getProviderConfiguration(): array
{
return parent::getProviderConfiguration() + [
'baseUrl' => 'https://keycloak_server/auth',
'realm' => 'your_master',
];
}

protected function getTestResponseForGetIdentity(): ResponseInterface
{
return $this->createResponse(
json_encode([
'sub' => 'uuid4',
])
);
}
}

0 comments on commit 03aecc5

Please sign in to comment.