Skip to content

Commit cdc238c

Browse files
✨ Add Keycloak PKCE Client (#458)
1 parent 2f48e1f commit cdc238c

File tree

6 files changed

+231
-0
lines changed

6 files changed

+231
-0
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ via Composer:
8787
| [Instagram](https://github.com/thephpleague/oauth2-instagram) | composer require league/oauth2-instagram |
8888
| [Jira](https://github.com/mrjoops/oauth2-jira) | composer require mrjoops/oauth2-jira |
8989
| [Keycloak](https://github.com/stevenmaguire/oauth2-keycloak) | composer require stevenmaguire/oauth2-keycloak |
90+
| [KeycloakPkce](https://github.com/stevenmaguire/oauth2-keycloak) | composer require stevenmaguire/oauth2-keycloak |
9091
| [LinkedIn](https://github.com/thephpleague/oauth2-linkedin) | composer require league/oauth2-linkedin |
9192
| [MailRu](https://github.com/rakeev/oauth2-mailru) | composer require aego/oauth2-mailru |
9293
| [Microsoft](https://github.com/stevenmaguire/oauth2-microsoft) | composer require stevenmaguire/oauth2-microsoft |
@@ -1212,6 +1213,33 @@ knpu_oauth2_client:
12121213
# whether to check OAuth2 "state": defaults to true
12131214
# use_state: true
12141215

1216+
# will create service: "knpu.oauth2.client.keycloak_pkce"
1217+
# an instance of: KnpU\OAuth2ClientBundle\Client\Provider\Pkce\KeycloakPkceClient
1218+
# composer require stevenmaguire/oauth2-keycloak
1219+
keycloak_pkce:
1220+
# must be "keycloak_pkce" - it activates that type!
1221+
type: keycloak_pkce
1222+
# add and set these environment variables in your .env files
1223+
client_id: '%env(OAUTH_KEYCLOAK_PKCE_CLIENT_ID)%'
1224+
client_secret: '%env(OAUTH_KEYCLOAK_PKCE_CLIENT_SECRET)%'
1225+
# a route name you'll create
1226+
redirect_route: connect_keycloak_pkce_check
1227+
redirect_params: {}
1228+
# Keycloak server URL
1229+
auth_server_url: null
1230+
# Keycloak realm
1231+
realm: null
1232+
# Optional: Encryption algorithm, i.e. RS256
1233+
# encryption_algorithm: null
1234+
# Optional: Encryption key path, i.e. ../key.pem
1235+
# encryption_key_path: null
1236+
# Optional: Encryption key, i.e. contents of key or certificate
1237+
# encryption_key: null
1238+
# Optional: The keycloak version to run against
1239+
# version: '20.0.1'
1240+
# whether to check OAuth2 "state": defaults to true
1241+
# use_state: true
1242+
12151243
# will create service: "knpu.oauth2.client.linkedin"
12161244
# an instance of: KnpU\OAuth2ClientBundle\Client\Provider\LinkedInClient
12171245
# composer require league/oauth2-linkedin

phpstan.neon.dist

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ parameters:
77
- */cache/*
88
ignoreErrors:
99
# False positive: clients are not dependencies of this project.
10+
-
11+
message: '#Method KnpU\\OAuth2ClientBundle\\Client\\Provider\\Pkce\\\w+::fetchUserFromToken\(\) has invalid return type .+#'
12+
path: ./src/Client/Provider/Pkce/
13+
-
14+
message: '#Method KnpU\\OAuth2ClientBundle\\Client\\Provider\\Pkce\\\w+::fetchUser\(\) has invalid return type .+#'
15+
path: ./src/Client/Provider/Pkce/
1016
-
1117
message: '#Method KnpU\\OAuth2ClientBundle\\Client\\Provider\\\w+::fetchUserFromToken\(\) has invalid return type .+#'
1218
path: ./src/Client/Provider/
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
/*
4+
* OAuth2 Client Bundle
5+
* Copyright (c) KnpUniversity <http://knpuniversity.com/>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace KnpU\OAuth2ClientBundle\Client\Provider\Pkce;
12+
13+
use KnpU\OAuth2ClientBundle\Client\OAuth2PKCEClient;
14+
use League\OAuth2\Client\Token\AccessToken;
15+
use Stevenmaguire\OAuth2\Client\Provider\KeycloakResourceOwner;
16+
17+
class KeycloakPkceClient extends OAuth2PKCEClient
18+
{
19+
/**
20+
* @return KeycloakResourceOwner|\League\OAuth2\Client\Provider\ResourceOwnerInterface
21+
*/
22+
public function fetchUserFromToken(AccessToken $accessToken)
23+
{
24+
return parent::fetchUserFromToken($accessToken);
25+
}
26+
27+
/**
28+
* @return KeycloakResourceOwner|\League\OAuth2\Client\Provider\ResourceOwnerInterface
29+
*/
30+
public function fetchUser()
31+
{
32+
return parent::fetchUser();
33+
}
34+
}

src/DependencyInjection/KnpUOAuth2ClientExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use KnpU\OAuth2ClientBundle\DependencyInjection\Providers\HerokuProviderConfigurator;
4545
use KnpU\OAuth2ClientBundle\DependencyInjection\Providers\InstagramProviderConfigurator;
4646
use KnpU\OAuth2ClientBundle\DependencyInjection\Providers\JiraProviderConfigurator;
47+
use KnpU\OAuth2ClientBundle\DependencyInjection\Providers\KeycloakPkceProviderConfigurator;
4748
use KnpU\OAuth2ClientBundle\DependencyInjection\Providers\KeycloakProviderConfigurator;
4849
use KnpU\OAuth2ClientBundle\DependencyInjection\Providers\LinkedInProviderConfigurator;
4950
use KnpU\OAuth2ClientBundle\DependencyInjection\Providers\MailRuProviderConfigurator;
@@ -128,6 +129,7 @@ class KnpUOAuth2ClientExtension extends Extension
128129
'instagram' => InstagramProviderConfigurator::class,
129130
'jira' => JiraProviderConfigurator::class,
130131
'keycloak' => KeycloakProviderConfigurator::class,
132+
'keycloak_pkce' => KeycloakPkceProviderConfigurator::class,
131133
'linkedin' => LinkedInProviderConfigurator::class,
132134
'mail_ru' => MailRuProviderConfigurator::class,
133135
'microsoft' => MicrosoftProviderConfigurator::class,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* OAuth2 Client Bundle
5+
* Copyright (c) KnpUniversity <http://knpuniversity.com/>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace KnpU\OAuth2ClientBundle\DependencyInjection\Providers;
12+
13+
use KnpU\OAuth2ClientBundle\Client\Provider\Pkce\KeycloakPkceClient;
14+
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
15+
16+
class KeycloakPkceProviderConfigurator implements ProviderConfiguratorInterface
17+
{
18+
public function buildConfiguration(NodeBuilder $node)
19+
{
20+
$node
21+
->scalarNode('auth_server_url')
22+
->isRequired()
23+
->info('Keycloak server URL')
24+
->end()
25+
->scalarNode('realm')
26+
->isRequired()
27+
->info('Keycloak realm')
28+
->end()
29+
->scalarNode('encryption_algorithm')
30+
->defaultNull()
31+
->info('Optional: Encryption algorithm, i.e. RS256')
32+
->end()
33+
->scalarNode('encryption_key_path')
34+
->defaultNull()
35+
->info('Optional: Encryption key path, i.e. ../key.pem')
36+
->end()
37+
->scalarNode('encryption_key')
38+
->defaultNull()
39+
->info('Optional: Encryption key, i.e. contents of key or certificate')
40+
->end()
41+
->scalarNode('version')
42+
->defaultNull()
43+
->info('Optional: The keycloak version to run against')
44+
->example("version: '20.0.1'")
45+
->end()
46+
;
47+
}
48+
49+
public function getProviderClass(array $config)
50+
{
51+
return 'Stevenmaguire\OAuth2\Client\Provider\Keycloak';
52+
}
53+
54+
public function getProviderOptions(array $config)
55+
{
56+
return [
57+
'clientId' => $config['client_id'],
58+
'clientSecret' => $config['client_secret'],
59+
'authServerUrl' => $config['auth_server_url'],
60+
'realm' => $config['realm'],
61+
'version' => $config['version'],
62+
'encryptionAlgorithm' => $config['encryption_algorithm'],
63+
'encryptionKeyPath' => $config['encryption_key_path'],
64+
'encryptionKey' => $config['encryption_key'],
65+
];
66+
}
67+
68+
public function getPackagistName()
69+
{
70+
return 'stevenmaguire/oauth2-keycloak';
71+
}
72+
73+
public function getLibraryHomepage()
74+
{
75+
return 'https://github.com/stevenmaguire/oauth2-keycloak';
76+
}
77+
78+
public function getProviderDisplayName()
79+
{
80+
return 'KeycloakPkce';
81+
}
82+
83+
public function getClientClass(array $config)
84+
{
85+
return KeycloakPkceClient::class;
86+
}
87+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/*
4+
* OAuth2 Client Bundle
5+
* Copyright (c) KnpUniversity <http://knpuniversity.com/>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
11+
namespace KnpU\OAuth2ClientBundle\Tests\Client\Provider;
12+
13+
use KnpU\OAuth2ClientBundle\Client\OAuth2PKCEClient;
14+
use League\OAuth2\Client\Provider\AbstractProvider;
15+
use League\OAuth2\Client\Provider\ResourceOwnerInterface;
16+
use League\OAuth2\Client\Token\AccessToken;
17+
use PHPUnit\Framework\TestCase;
18+
use Symfony\Component\HttpFoundation\Request;
19+
use Symfony\Component\HttpFoundation\RequestStack;
20+
use Symfony\Component\HttpFoundation\Session\SessionInterface;
21+
22+
class BatchPkceProviderTest extends TestCase
23+
{
24+
public function testProviders()
25+
{
26+
// This is basically just validating that the clients are sane/implementing OAuth2PkceClient
27+
28+
$mockAccessToken = $this->getMockBuilder(AccessToken::class)->disableOriginalConstructor()->getMock();
29+
$mockProvider = $this->getMockProvider($mockAccessToken);
30+
$mockRequestStack = $this->getMockRequestStack($this->getMockRequest());
31+
32+
$clients = scandir(__DIR__ . "/../../../src/Client/Provider/Pkce");
33+
foreach($clients as $client) {
34+
if(substr($client, -4, 4) !== ".php") { continue; }
35+
36+
$client = sprintf("KnpU\OAuth2ClientBundle\Client\Provider\Pkce\%s", explode(".", $client)[0]);
37+
$testClient = new $client($mockProvider, $mockRequestStack);
38+
$testClient->setAsStateless();
39+
$this->assertTrue(is_subclass_of($testClient, OAuth2PKCEClient::class));
40+
41+
$this->assertInstanceOf(ResourceOwnerInterface::class, $testClient->fetchUserFromToken($mockAccessToken));
42+
$this->assertInstanceOf(ResourceOwnerInterface::class, $testClient->fetchUser());
43+
}
44+
}
45+
46+
private function getMockProvider($mockAccessToken)
47+
{
48+
$mockProvider = $this->getMockBuilder(AbstractProvider::class)->getMock();
49+
$mockProvider->method("getResourceOwner")->willReturn($this->getMockBuilder(ResourceOwnerInterface::class)->getMock());
50+
$mockProvider->method("getAccessToken")->willReturn($mockAccessToken);
51+
return $mockProvider;
52+
}
53+
54+
private function getMockRequest()
55+
{
56+
$mockRequest = $this->getMockBuilder(Request::class)->getMock();
57+
58+
$session = $this->getMockBuilder(SessionInterface::class)->getMock();
59+
$session->method("has")->with(OAuth2PKCEClient::VERIFIER_KEY)->willReturn(true);
60+
$session->method("get")->with(OAuth2PKCEClient::VERIFIER_KEY)->willReturn('test_code_verifier');
61+
62+
$mockRequest->method("getSession")->willReturn($session);
63+
64+
$mockRequest->method("get")->willReturn(true);
65+
return $mockRequest;
66+
}
67+
68+
private function getMockRequestStack($mockRequest)
69+
{
70+
$mockRequestStack = $this->getMockBuilder(RequestStack::class)->getMock();
71+
$mockRequestStack->method("getCurrentRequest")->willReturn($mockRequest);
72+
return $mockRequestStack;
73+
}
74+
}

0 commit comments

Comments
 (0)