From 03aecc594bc62cd43322d01d853ae800b94116e5 Mon Sep 17 00:00:00 2001 From: gguseynov Date: Wed, 16 Sep 2020 10:21:30 +0200 Subject: [PATCH] keycloak openidconnect provider added --- example/config.php.dist | 11 ++ src/Auth/CollectionFactory.php | 1 + src/OpenIDConnect/Provider/Keycloak.php | 148 ++++++++++++++++++ .../OpenIDConnect/Provider/KeycloakTest.php | 33 ++++ 4 files changed, 193 insertions(+) create mode 100644 src/OpenIDConnect/Provider/Keycloak.php create mode 100644 tests/Test/OpenIDConnect/Provider/KeycloakTest.php diff --git a/example/config.php.dist b/example/config.php.dist index fc0abcc7d..7f1cd619f 100644 --- a/example/config.php.dist +++ b/example/config.php.dist @@ -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 + ], + ], ] ]; diff --git a/src/Auth/CollectionFactory.php b/src/Auth/CollectionFactory.php index e17f94e92..ad2592249 100644 --- a/src/Auth/CollectionFactory.php +++ b/src/Auth/CollectionFactory.php @@ -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, ]; diff --git a/src/OpenIDConnect/Provider/Keycloak.php b/src/OpenIDConnect/Provider/Keycloak.php new file mode 100644 index 000000000..1ea6c3319 --- /dev/null +++ b/src/OpenIDConnect/Provider/Keycloak.php @@ -0,0 +1,148 @@ +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); + } +} diff --git a/tests/Test/OpenIDConnect/Provider/KeycloakTest.php b/tests/Test/OpenIDConnect/Provider/KeycloakTest.php new file mode 100644 index 000000000..35da639b4 --- /dev/null +++ b/tests/Test/OpenIDConnect/Provider/KeycloakTest.php @@ -0,0 +1,33 @@ + 'https://keycloak_server/auth', + 'realm' => 'your_master', + ]; + } + + protected function getTestResponseForGetIdentity(): ResponseInterface + { + return $this->createResponse( + json_encode([ + 'sub' => 'uuid4', + ]) + ); + } +}