[Web] Update composer libraries

This commit is contained in:
FreddleSpl0it committed 2026-10-28 11:56:29 +01:00
1 parent 9cc8ac2908
commit bd8c784118
1297 files changed
+71585 -24460

No files matched your search

@@ -7,8 +7,6 @@ We accept contributions via Pull Requests on [Github](https://github.com/stevenm
## Pull Requests
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the README and any other relevant documentation are kept up-to-date.
@@ -29,14 +27,7 @@ We accept contributions via Pull Requests on [Github](https://github.com/stevenm
## Running Tests
``` bash
$ ./vendor/bin/phpunit
```
## Running PHP Code Sniffer
``` bash
$ ./vendor/bin/phpcs src --standard=psr2 -sp
$ composer test
```
**Happy coding**!
@@ -1,7 +1,6 @@
# Keycloak Provider for OAuth 2.0 Client
[![Latest Version](https://img.shields.io/github/release/stevenmaguire/oauth2-keycloak.svg?style=flat-square)](https://github.com/stevenmaguire/oauth2-keycloak/releases)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md)
[![Build Status](https://img.shields.io/travis/stevenmaguire/oauth2-keycloak/master.svg?style=flat-square)](https://travis-ci.org/stevenmaguire/oauth2-keycloak)
[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/stevenmaguire/oauth2-keycloak.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/oauth2-keycloak/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/stevenmaguire/oauth2-keycloak.svg?style=flat-square)](https://scrutinizer-ci.com/g/stevenmaguire/oauth2-keycloak)
[![Total Downloads](https://img.shields.io/packagist/dt/stevenmaguire/oauth2-keycloak.svg?style=flat-square)](https://packagist.org/packages/stevenmaguire/oauth2-keycloak)
@@ -20,9 +19,11 @@ composer require stevenmaguire/oauth2-keycloak
Usage is the same as The League's OAuth client, using `\Stevenmaguire\OAuth2\Client\Provider\Keycloak` as the provider.
Use `authServerUrl` to specify the Keycloak server URL. You can lookup the correct value from the Keycloak client installer JSON under `auth-server-url`, eg. `http://localhost:8080/auth`.
Use `authServerUrl` to specify the Keycloak server URL. This depends on your Keycloak version:
- legacy releases often use `http://localhost:8080/auth`
- modern releases (Keycloak 17+) typically use `http://localhost:8080`
Use `realm` to specify the Keycloak realm name. You can lookup the correct value from the Keycloak client installer JSON under `resource`, eg. `master`.
Use `realm` to specify the Keycloak realm name. In client configuration exports this is represented as `realm`, e.g. `master`.
### Authorization Code Flow
@@ -34,8 +35,8 @@ $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'clientSecret' => '{keycloak-client-secret}',
'redirectUri' => 'https://example.com/callback-url',
'encryptionAlgorithm' => 'RS256', // optional
'encryptionKeyPath' => '../key.pem' // optional
'encryptionKey' => 'contents_of_key_or_certificate' // optional
'encryptionKeyPath' => '../key.pem', // optional
'encryptionKey' => 'contents_of_key_or_certificate', // optional
'version' => '20.0.1', // optional
]);
@@ -55,7 +56,7 @@ if (!isset($_GET['code'])) {
} else {
// Try to get an access token (using the authorization coe grant)
// Try to get an access token (using the authorization code grant)
try {
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
@@ -155,8 +156,8 @@ $provider->setEncryptionKeyPath($keyPath);
## Testing
``` bash
$ ./vendor/bin/phpunit
```bash
$ composer test
```
## Contributing
@@ -18,14 +18,15 @@
"keycloak"
],
"require": {
"php": "~7.2 || ~8.0",
"league/oauth2-client": "^2.0",
"firebase/php-jwt": "^4.0 || ^5.0 || ^6.0"
"php": "^8.0",
"league/oauth2-client": "^2.8",
"firebase/php-jwt": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "~9.6.4",
"mockery/mockery": "~1.5.0",
"squizlabs/php_codesniffer": "~3.7.0"
"mockery/mockery": "^1.6",
"squizlabs/php_codesniffer": "~3.7.0",
"phpstan/phpstan": "^1.12"
},
"autoload": {
"psr-4": {
@@ -37,6 +38,11 @@
"Stevenmaguire\\OAuth2\\Client\\Test\\": "test/src/"
}
},
"config": {
"platform": {
"php": "8.0.0"
}
},
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
@@ -48,4 +54,4 @@
"phpunit --colors=always"
]
}
}
}
@@ -0,0 +1,4 @@
parameters:
level: max
paths:
- src
@@ -11,9 +11,9 @@ class EncryptionConfigurationException extends Exception
*
* @return \Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException
*/
public static function undeterminedEncryption()
public static function undeterminedEncryption(): self
{
return new static(
return new self(
'The given response may be encrypted and sufficient '.
'encryption configuration has not been provided.',
400
@@ -4,6 +4,7 @@ namespace Stevenmaguire\OAuth2\Client\Provider;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
@@ -37,43 +38,43 @@ class Keycloak extends AbstractProvider
* https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
* for a list of spec-compliant algorithms.
*
* @var string
* @var string|null
*/
public $encryptionAlgorithm = null;
/**
* Encryption key.
*
* @var string
* @var string|null
*/
public $encryptionKey = null;
/**
* Keycloak version.
*
* @var string
*/
* Keycloak version.
*
* @var string|null
*/
public $version = null;
/**
* Constructs an OAuth 2.0 service provider.
*
* @param array $options An array of options to set on this provider.
* @param array<string, mixed> $options An array of options to set on this provider.
* Options include `clientId`, `clientSecret`, `redirectUri`, and `state`.
* Individual providers may introduce more options, as needed.
* @param array $collaborators An array of collaborators that may be used to
* @param array<string, mixed> $collaborators An array of collaborators that may be used to
* override this provider's default behavior. Collaborators include
* `grantFactory`, `requestFactory`, `httpClient`, and `randomFactory`.
* Individual providers may introduce more collaborators, as needed.
*/
public function __construct(array $options = [], array $collaborators = [])
{
if (isset($options['encryptionKeyPath'])) {
if (isset($options['encryptionKeyPath']) && is_string($options['encryptionKeyPath'])) {
$this->setEncryptionKeyPath($options['encryptionKeyPath']);
unset($options['encryptionKeyPath']);
}
if (isset($options['version'])) {
if (isset($options['version']) && is_string($options['version'])) {
$this->setVersion($options['version']);
}
@@ -83,27 +84,39 @@ class Keycloak extends AbstractProvider
/**
* Attempts to decrypt the given response.
*
* @param string|array|null $response
* @param string|array<string, mixed>|null $response
*
* @return string|array|null
* @return string|array<string, mixed>|null
* @throws EncryptionConfigurationException
*/
public function decryptResponse($response)
public function decryptResponse($response): array|string|null
{
if (!is_string($response)) {
return $response;
}
if ($this->usesEncryption()) {
return json_decode(
json_encode(
JWT::decode(
$response,
$this->encryptionKey,
array($this->encryptionAlgorithm)
)
),
true
if (!is_string($this->encryptionKey) || !is_string($this->encryptionAlgorithm)) {
throw EncryptionConfigurationException::undeterminedEncryption();
}
$tokenData = JWT::decode(
$response,
new Key(
$this->encryptionKey,
$this->encryptionAlgorithm
)
);
$tokenJson = json_encode($tokenData);
if (!is_string($tokenJson)) {
throw EncryptionConfigurationException::undeterminedEncryption();
}
$decodedToken = json_decode($tokenJson, true);
if (!is_array($decodedToken)) {
throw EncryptionConfigurationException::undeterminedEncryption();
}
return $decodedToken;
}
throw EncryptionConfigurationException::undeterminedEncryption();
@@ -114,7 +127,7 @@ class Keycloak extends AbstractProvider
*
* @return string
*/
public function getBaseAuthorizationUrl()
public function getBaseAuthorizationUrl(): string
{
return $this->getBaseUrlWithRealm().'/protocol/openid-connect/auth';
}
@@ -122,11 +135,11 @@ class Keycloak extends AbstractProvider
/**
* Get access token url to retrieve token
*
* @param array $params
* @param array<string, mixed> $params
*
* @return string
*/
public function getBaseAccessTokenUrl(array $params)
public function getBaseAccessTokenUrl(array $params): string
{
return $this->getBaseUrlWithRealm().'/protocol/openid-connect/token';
}
@@ -138,7 +151,7 @@ class Keycloak extends AbstractProvider
*
* @return string
*/
public function getResourceOwnerDetailsUrl(AccessToken $token)
public function getResourceOwnerDetailsUrl(AccessToken $token): string
{
return $this->getBaseUrlWithRealm().'/protocol/openid-connect/userinfo';
}
@@ -146,10 +159,10 @@ class Keycloak extends AbstractProvider
/**
* Builds the logout URL.
*
* @param array $options
* @param array<string, mixed> $options
* @return string Authorization URL
*/
public function getLogoutUrl(array $options = [])
public function getLogoutUrl(array $options = []): string
{
$base = $this->getBaseLogoutUrl();
$params = $this->getAuthorizationParameters($options);
@@ -158,10 +171,13 @@ class Keycloak extends AbstractProvider
// As of this version the parameter is called post_logout_redirect_uri. In addition to this
// a parameter id_token_hint has to be provided.
if ($this->validateGteVersion('18.0.0')) {
if (isset($options['access_token']) === true) {
if (isset($options['access_token']) === true && $options['access_token'] instanceof AccessToken) {
$accessToken = $options['access_token'];
$values = $accessToken->getValues();
$params['id_token_hint'] = $accessToken->getValues()['id_token'];
if (isset($values['id_token']) && is_string($values['id_token'])) {
$params['id_token_hint'] = $values['id_token'];
}
$params['post_logout_redirect_uri'] = $params['redirect_uri'];
}
@@ -177,7 +193,7 @@ class Keycloak extends AbstractProvider
*
* @return string
*/
private function getBaseLogoutUrl()
private function getBaseLogoutUrl(): string
{
return $this->getBaseUrlWithRealm() . '/protocol/openid-connect/logout';
}
@@ -187,7 +203,7 @@ class Keycloak extends AbstractProvider
*
* @return string
*/
protected function getBaseUrlWithRealm()
protected function getBaseUrlWithRealm(): string
{
return $this->authServerUrl.'/realms/'.$this->realm;
}
@@ -200,7 +216,7 @@ class Keycloak extends AbstractProvider
*
* @return string[]
*/
protected function getDefaultScopes()
protected function getDefaultScopes(): array
{
$scopes = [
'profile',
@@ -218,7 +234,7 @@ class Keycloak extends AbstractProvider
*
* @return string Scope separator, defaults to ','
*/
protected function getScopeSeparator()
protected function getScopeSeparator(): string
{
return ' ';
}
@@ -229,28 +245,32 @@ class Keycloak extends AbstractProvider
*
* @throws IdentityProviderException
* @param ResponseInterface $response
* @param string $data Parsed response data
* @param mixed $data Parsed response data
* @return void
*/
protected function checkResponse(ResponseInterface $response, $data)
protected function checkResponse(ResponseInterface $response, $data): void
{
if (!is_array($data)) {
return;
}
if (!empty($data['error'])) {
$error = $data['error'];
if (isset($data['error_description'])) {
$error.=': '.$data['error_description'];
$error .= ': '.$data['error_description'];
}
throw new IdentityProviderException($error, 0, $data);
throw new IdentityProviderException($error, $response->getStatusCode(), $data);
}
}
/**
* Generate a user object from a successful user details request.
*
* @param array $response
* @param array<string, mixed> $response
* @param AccessToken $token
* @return KeycloakResourceOwner
*/
protected function createResourceOwner(array $response, AccessToken $token)
protected function createResourceOwner(array $response, AccessToken $token): KeycloakResourceOwner
{
return new KeycloakResourceOwner($response);
}
@@ -262,9 +282,12 @@ class Keycloak extends AbstractProvider
* @return KeycloakResourceOwner
* @throws EncryptionConfigurationException
*/
public function getResourceOwner(AccessToken $token)
public function getResourceOwner(AccessToken $token): KeycloakResourceOwner
{
$response = $this->fetchResourceOwnerDetails($token);
if (!is_array($response)) {
throw new UnexpectedValueException('Expected resource owner details to be an array.');
}
// We are always getting an array. We have to check if it is
// the array we created
@@ -273,6 +296,9 @@ class Keycloak extends AbstractProvider
}
$response = $this->decryptResponse($response);
if (!is_array($response)) {
throw new UnexpectedValueException('Expected decrypted resource owner details to be an array.');
}
return $this->createResourceOwner($response, $token);
}
@@ -284,7 +310,7 @@ class Keycloak extends AbstractProvider
*
* @return Keycloak
*/
public function setEncryptionAlgorithm($encryptionAlgorithm)
public function setEncryptionAlgorithm(string $encryptionAlgorithm): self
{
$this->encryptionAlgorithm = $encryptionAlgorithm;
@@ -298,7 +324,7 @@ class Keycloak extends AbstractProvider
*
* @return Keycloak
*/
public function setEncryptionKey($encryptionKey)
public function setEncryptionKey(string $encryptionKey): self
{
$this->encryptionKey = $encryptionKey;
@@ -313,10 +339,13 @@ class Keycloak extends AbstractProvider
*
* @return Keycloak
*/
public function setEncryptionKeyPath($encryptionKeyPath)
public function setEncryptionKeyPath(string $encryptionKeyPath): self
{
try {
$this->encryptionKey = file_get_contents($encryptionKeyPath);
$encryptionKey = file_get_contents($encryptionKeyPath);
if (is_string($encryptionKey)) {
$this->encryptionKey = $encryptionKey;
}
} catch (Exception $e) {
// Not sure how to handle this yet.
}
@@ -331,7 +360,7 @@ class Keycloak extends AbstractProvider
*
* @return Keycloak
*/
public function setVersion($version)
public function setVersion(string $version): self
{
$this->version = $version;
@@ -343,7 +372,7 @@ class Keycloak extends AbstractProvider
*
* @return bool
*/
public function usesEncryption()
public function usesEncryption(): bool
{
return (bool) $this->encryptionAlgorithm && $this->encryptionKey;
}
@@ -353,16 +382,15 @@ class Keycloak extends AbstractProvider
*
* @throws UnexpectedValueException
* @param ResponseInterface $response
* @return array
* @return string|array<string, mixed>
*/
protected function parseResponse(ResponseInterface $response)
protected function parseResponse(ResponseInterface $response): string|array
{
// We have a problem with keycloak when the userinfo responses
// with a jwt token
// Because it just return a jwt as string with the header
// application/jwt
// This can't be parsed to a array
// Dont know why this function only allow an array as return value...
$content = (string) $response->getBody();
$type = $this->getContentType($response);
@@ -380,8 +408,8 @@ class Keycloak extends AbstractProvider
* @param string $version
* @return bool
*/
private function validateGteVersion($version)
private function validateGteVersion(string $version): bool
{
return (isset($this->version) && version_compare($this->version, $version, '>='));
return is_string($this->version) && version_compare($this->version, $version, '>=');
}
}
@@ -9,14 +9,14 @@ class KeycloakResourceOwner implements ResourceOwnerInterface
/**
* Raw response
*
* @var array
* @var array<string, mixed>
*/
protected $response;
/**
* Creates new resource owner.
*
* @param array $response
* @param array<string, mixed> $response
*/
public function __construct(array $response = array())
{
@@ -28,9 +28,16 @@ class KeycloakResourceOwner implements ResourceOwnerInterface
*
* @return string|null
*/
public function getId()
public function getId(): ?string
{
return \array_key_exists('sub', $this->response) ? $this->response['sub'] : null;
if (!\array_key_exists('sub', $this->response)) {
return null;
}
if (!is_scalar($this->response['sub'])) {
return null;
}
return (string) $this->response['sub'];
}
/**
@@ -38,9 +45,13 @@ class KeycloakResourceOwner implements ResourceOwnerInterface
*
* @return string|null
*/
public function getEmail()
public function getEmail(): ?string
{
return \array_key_exists('email', $this->response) ? $this->response['email'] : null;
if (!\array_key_exists('email', $this->response) || !is_string($this->response['email'])) {
return null;
}
return $this->response['email'];
}
/**
@@ -48,17 +59,63 @@ class KeycloakResourceOwner implements ResourceOwnerInterface
*
* @return string|null
*/
public function getName()
public function getName(): ?string
{
return \array_key_exists('name', $this->response) ? $this->response['name'] : null;
if (!\array_key_exists('name', $this->response) || !is_string($this->response['name'])) {
return null;
}
return $this->response['name'];
}
/**
* Get resource owner username
*
* @return string|null
*/
public function getUsername(): ?string
{
if (!\array_key_exists('preferred_username', $this->response) || !is_string($this->response['preferred_username'])) {
return null;
}
return $this->response['preferred_username'];
}
/**
* Get resource owner first name
*
* @return string|null
*/
public function getFirstName(): ?string
{
if (!\array_key_exists('given_name', $this->response) || !is_string($this->response['given_name'])) {
return null;
}
return $this->response['given_name'];
}
/**
* Get resource owner last name
*
* @return string|null
*/
public function getLastName(): ?string
{
if (!\array_key_exists('family_name', $this->response) || !is_string($this->response['family_name'])) {
return null;
}
return $this->response['family_name'];
}
/**
* Return all of the owner details available as an array.
*
* @return array
* @return array<string, mixed>
*/
public function toArray()
public function toArray(): array
{
return $this->response;
}
@@ -7,7 +7,7 @@ namespace
namespace Stevenmaguire\OAuth2\Client\Provider
{
function file_get_contents()
function file_get_contents(...$args): string|false
{
global $mockFileGetContents;
if (isset($mockFileGetContents) && ! is_null($mockFileGetContents)) {
@@ -16,17 +16,22 @@ namespace Stevenmaguire\OAuth2\Client\Provider
}
return $mockFileGetContents;
} else {
return call_user_func_array('\file_get_contents', func_get_args());
return \file_get_contents(...$args);
}
}
}
namespace Stevenmaguire\OAuth2\Client\Test\Provider
{
use DateInterval;
use DateTimeImmutable;
use Firebase\JWT\JWT;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\QueryBuilderTrait;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use Psr\Http\Message\StreamInterface;
use Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException;
use Stevenmaguire\OAuth2\Client\Provider\Keycloak;
@@ -34,7 +39,59 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
{
use QueryBuilderTrait;
protected $provider;
public const ENCRYPTION_KEY = <<<EOD
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8kGa1pSjbSYZVebtTRBLxBz5H
4i2p/llLCrEeQhta5kaQu/RnvuER4W8oDH3+3iuIYW4VQAzyqFpwuzjkDI+17t5t
0tyazyZ8JXw+KgXTxldMPEL95+qVhgXvwtihXC1c5oGbRlEDvDF6Sa53rcFVsYJ4
ehde/zUxo6UvS7UrBQIDAQAB
-----END PUBLIC KEY-----
EOD;
public const ENCRYPTION_ALGORITHM = 'HS256';
private string $jwtTemplate = <<<EOF
{
"exp": "%s",
"iat": "%s",
"jti": "e11a85c8-aa91-4f75-9088-57db4586f8b9",
"iss": "https://example.org/auth/realms/test-realm",
"aud": "account",
"nbf": "%s",
"sub": "4332085e-b944-4acc-9eb1-27d8f5405f3e",
"typ": "Bearer",
"azp": "test-app",
"session_state": "c90c8e0d-aabb-4c71-b8a8-e88792cacd96",
"acr": "1",
"realm_access": {
"roles": [
"default-roles-test-realm",
"offline_access",
"uma_authorization"
]
},
"resource_access": {
"account": {
"roles": [
"manage-account",
"manage-account-links",
"view-profile"
]
}
},
"scope": "openid email profile",
"sid": "c90c8e0d-aabb-4c71-b8a8-e88792cacd96",
"address": {},
"email_verified": true,
"name": "Test User",
"preferred_username": "test-user",
"given_name": "Test",
"family_name": "User",
"email": "test-user@example.org"
}
EOF;
protected Keycloak $provider;
protected function setUp(): void
{
@@ -53,7 +110,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
parent::tearDown();
}
public function testAuthorizationUrl()
public function testAuthorizationUrl(): void
{
$url = $this->provider->getAuthorizationUrl();
$uri = parse_url($url);
@@ -68,7 +125,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertNotNull($this->provider->getState());
}
public function testEncryptionAlgorithm()
public function testEncryptionAlgorithm(): void
{
$algorithm = uniqid();
$provider = new Keycloak([
@@ -83,7 +140,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals($algorithm, $provider->encryptionAlgorithm);
}
public function testEncryptionKey()
public function testEncryptionKey(): void
{
$key = uniqid();
$provider = new Keycloak([
@@ -98,7 +155,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals($key, $provider->encryptionKey);
}
public function testEncryptionKeyPath()
public function testEncryptionKeyPath(): void
{
global $mockFileGetContents;
$path = uniqid();
@@ -120,7 +177,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals($key, $provider->encryptionKey);
}
public function testEncryptionKeyPathFails()
public function testEncryptionKeyPathFails(): void
{
$this->markTestIncomplete('Need to assess the test to see what is required to be checked.');
@@ -136,7 +193,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$provider->setEncryptionKeyPath($path);
}
public function testScopes()
public function testScopes(): void
{
$scopeSeparator = ' ';
$options = ['scope' => [uniqid(), uniqid()]];
@@ -146,7 +203,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertStringContainsString($encodedScope, $url);
}
public function testGetAuthorizationUrl()
public function testGetAuthorizationUrl(): void
{
$url = $this->provider->getAuthorizationUrl();
$uri = parse_url($url);
@@ -154,7 +211,7 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals('/auth/realms/mock_realm/protocol/openid-connect/auth', $uri['path']);
}
public function testGetLogoutUrl()
public function testGetLogoutUrl(): void
{
$url = $this->provider->getLogoutUrl();
$uri = parse_url($url);
@@ -162,7 +219,26 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals('/auth/realms/mock_realm/protocol/openid-connect/logout', $uri['path']);
}
public function testGetBaseAccessTokenUrl()
public function testGetLogoutUrlWithIdTokenHint(): void
{
$this->provider->setVersion('18.0.0');
$options = [
'access_token' => new AccessToken(
[
'id_token' => 'the_id_token',
'access_token' => 'the_access_token',
]
),
];
$url = $this->provider->getLogoutUrl($options);
$uri = parse_url($url);
$this->assertEquals('/auth/realms/mock_realm/protocol/openid-connect/logout', $uri['path']);
$this->assertStringContainsString('id_token_hint=the_id_token', $uri['query']);
}
public function testGetBaseAccessTokenUrl(): void
{
$params = [];
@@ -172,12 +248,19 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals('/auth/realms/mock_realm/protocol/openid-connect/token', $uri['path']);
}
public function testGetAccessToken()
public function testGetAccessToken(): void
{
$stream = $this->createMock(StreamInterface::class);
$stream
->method('__toString')
->willReturn('{"access_token":"mock_access_token","scope":"email","token_type":"bearer"}');
$response = m::mock('Psr\Http\Message\ResponseInterface');
$response->shouldReceive('getBody')
->andReturn('{"access_token":"mock_access_token", "scope":"email", "token_type":"bearer"}');
$response->shouldReceive('getHeader')
$response
->shouldReceive('getBody')
->andReturn($stream);
$response
->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$client = m::mock('GuzzleHttp\ClientInterface');
@@ -186,7 +269,9 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
->andReturn($response);
$this->provider->setHttpClient($client);
$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
$token = $this
->provider
->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
$this->assertEquals('mock_access_token', $token->getToken());
$this->assertNull($token->getExpires());
@@ -194,34 +279,66 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertNull($token->getResourceOwnerId());
}
public function testUserData()
public function testUserData(): void
{
$userId = rand(1000, 9999);
$name = uniqid();
$nickname = uniqid();
$email = uniqid();
$username = uniqid();
$firstName = uniqid();
$lastName = uniqid();
$getAccessTokenResponseStream = $this->createMock(StreamInterface::class);
$getAccessTokenResponseStream
->method('__toString')
->willReturn(
'{"access_token":"mock_access_token","expires":"3600","refresh_token":"mock_refresh_token","otherKey":[1234]}'
);
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')
->andReturn(
'access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}'
$postResponse
->shouldReceive('getBody')
->andReturn($getAccessTokenResponseStream);
$postResponse
->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$getResourceOwnerResponseStream = $this->createMock(StreamInterface::class);
$getResourceOwnerResponseStream
->method('__toString')
->willReturn(
sprintf(
'{"sub": "%s", "name": "%s", "email": "%s", "preferred_username": "%s", "given_name": "%s", "family_name": "%s"}',
$userId,
$name,
$email,
$username,
$firstName,
$lastName
)
);
$postResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')
->andReturn('{"sub": '.$userId.', "name": "'.$name.'", "email": "'.$email.'"}');
$userResponse->shouldReceive('getHeader')
$userResponse
->shouldReceive('getBody')
->andReturn($getResourceOwnerResponseStream);
$userResponse
->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
->times(2)
$client
->shouldReceive('send')
->andReturn($postResponse, $userResponse);
$this->provider->setHttpClient($client);
$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
$token = $this->provider->getAccessToken(
'authorization_code',
[
'code' => 'mock_authorization_code',
'access_token' => 'mock_access_token',
]
);
$user = $this->provider->getResourceOwner($token);
$this->assertEquals($userId, $user->getId());
@@ -230,113 +347,219 @@ namespace Stevenmaguire\OAuth2\Client\Test\Provider
$this->assertEquals($name, $user->toArray()['name']);
$this->assertEquals($email, $user->getEmail());
$this->assertEquals($email, $user->toArray()['email']);
$this->assertEquals($username, $user->getUsername());
$this->assertEquals($username, $user->toArray()['preferred_username']);
$this->assertEquals($firstName, $user->getFirstName());
$this->assertEquals($firstName, $user->toArray()['given_name']);
$this->assertEquals($lastName, $user->getLastName());
$this->assertEquals($lastName, $user->toArray()['family_name']);
}
public function testUserDataWithEncryption()
public function testUserDataWithEncryption(): void
{
$userId = rand(1000, 9999);
$name = uniqid();
$nickname = uniqid();
$email = uniqid();
$jwt = uniqid();
$algorithm = uniqid();
$key = uniqid();
$jwt = JWT::encode(
json_decode(
sprintf(
$this->jwtTemplate,
(new DateTimeImmutable())->add(new DateInterval('PT1H'))->getTimestamp(),
(new DateTimeImmutable())->sub(new DateInterval('P1D'))->getTimestamp(),
(new DateTimeImmutable())->sub(new DateInterval('P1D'))->getTimestamp()
),
true
),
self::ENCRYPTION_KEY,
self::ENCRYPTION_ALGORITHM
);
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')
->andReturn(
'access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}'
$getAccessTokenResponseStream = $this->createMock(StreamInterface::class);
$getAccessTokenResponseStream
->method('__toString')
->willReturn(
sprintf(
'{"access_token":"%s","expires":"3600","refresh_token":"mock_refresh_token","otherKey":[1234]}',
$jwt
)
);
$postResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$postResponse->shouldReceive('getStatusCode')
$accessTokenResponse = m::mock('Psr\Http\Message\ResponseInterface');
$accessTokenResponse
->shouldReceive('getBody')
->andReturn($getAccessTokenResponseStream);
$accessTokenResponse
->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$accessTokenResponse
->shouldReceive('getStatusCode')
->andReturn(200);
$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')
->andReturn($jwt);
$userResponse->shouldReceive('getHeader')
$getResourceOwnerResponseStream = $this->createMock(StreamInterface::class);
$getResourceOwnerResponseStream
->method('__toString')
->willReturn($jwt);
$resourceOwnerResponse = m::mock('Psr\Http\Message\ResponseInterface');
$resourceOwnerResponse
->shouldReceive('getBody')
->andReturn($getResourceOwnerResponseStream);
$resourceOwnerResponse
->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/jwt']);
$userResponse->shouldReceive('getStatusCode')
$resourceOwnerResponse
->shouldReceive('getStatusCode')
->andReturn(200);
$decoder = \Mockery::mock('overload:Firebase\JWT\JWT');
$decoder->shouldReceive('decode')
->with($jwt, $key, [$algorithm])
->andReturn([
'sub' => $userId,
'email' => $email,
'name' => $name,
]);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
$client
->shouldReceive('send')
->times(2)
->andReturn($postResponse, $userResponse);
->andReturn($accessTokenResponse, $resourceOwnerResponse);
$this->provider->setHttpClient($client);
$token = $this->provider->setEncryptionAlgorithm($algorithm)
->setEncryptionKey($key)
$token = $this
->provider
->setEncryptionAlgorithm(self::ENCRYPTION_ALGORITHM)
->setEncryptionKey(self::ENCRYPTION_KEY)
->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
$user = $this->provider->getResourceOwner($token);
$email = "test-user@example.org";
$name = "Test User";
$userId = "4332085e-b944-4acc-9eb1-27d8f5405f3e";
$username = "test-user";
$firstName = "Test";
$lastName = "User";
$this->assertEquals($userId, $user->getId());
$this->assertEquals($userId, $user->toArray()['sub']);
$this->assertEquals($name, $user->getName());
$this->assertEquals($name, $user->toArray()['name']);
$this->assertEquals($email, $user->getEmail());
$this->assertEquals($email, $user->toArray()['email']);
$this->assertEquals($username, $user->getUsername());
$this->assertEquals($username, $user->toArray()['preferred_username']);
$this->assertEquals($firstName, $user->getFirstName());
$this->assertEquals($firstName, $user->toArray()['given_name']);
$this->assertEquals($lastName, $user->getLastName());
$this->assertEquals($lastName, $user->toArray()['family_name']);
}
public function testUserDataFailsWhenEncryptionEncounteredAndNotConfigured()
public function testUserDataFailsWhenEncryptionEncounteredAndNotConfigured(): void
{
$this->expectException(EncryptionConfigurationException::class);
$postResponse = m::mock('Psr\Http\Message\ResponseInterface');
$postResponse->shouldReceive('getBody')
->andReturn(
'access_token=mock_access_token&expires=3600&refresh_token=mock_refresh_token&otherKey={1234}'
$accessTokenResponseStream = $this->createMock(StreamInterface::class);
$accessTokenResponseStream
->method('__toString')
->willReturn(
'{"access_token":"mock_access_token","expires":"3600","refresh_token":"mock_refresh_token","otherKey":[1234]}'
);
$postResponse->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/x-www-form-urlencoded']);
$postResponse->shouldReceive('getStatusCode')
$getAccessTokenResponse = m::mock('Psr\Http\Message\ResponseInterface');
$getAccessTokenResponse
->shouldReceive('getBody')
->andReturn($accessTokenResponseStream);
$getAccessTokenResponse
->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$getAccessTokenResponse
->shouldReceive('getStatusCode')
->andReturn(200);
$userResponse = m::mock('Psr\Http\Message\ResponseInterface');
$userResponse->shouldReceive('getBody')
->andReturn(uniqid());
$userResponse->shouldReceive('getHeader')
$resourceOwnerResponseStream = $this->createMock(StreamInterface::class);
$resourceOwnerResponseStream
->method('__toString')
->willReturn(uniqid());
$getResourceOwnerResponse = m::mock('Psr\Http\Message\ResponseInterface');
$getResourceOwnerResponse
->shouldReceive('getBody')
->andReturn($resourceOwnerResponseStream);
$getResourceOwnerResponse
->shouldReceive('getHeader')
->andReturn(['content-type' => 'application/jwt']);
$userResponse->shouldReceive('getStatusCode')
$getResourceOwnerResponse
->shouldReceive('getStatusCode')
->andReturn(200);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
$client
->shouldReceive('send')
->times(2)
->andReturn($postResponse, $userResponse);
->andReturn($getAccessTokenResponse, $getResourceOwnerResponse);
$this->provider->setHttpClient($client);
$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
$token = $this->provider->getAccessToken(
'authorization_code', #
['code' => 'mock_authorization_code']
);
$user = $this->provider->getResourceOwner($token);
}
public function testErrorResponse()
public function testErrorResponse(): void
{
$this->expectException(IdentityProviderException::class);
$accessTokenResponseStream = $this->createMock(StreamInterface::class);
$accessTokenResponseStream
->method('__toString')
->willReturn(
'{"error": "invalid_grant", "error_description": "Code not found"}'
);
$response = m::mock('Psr\Http\Message\ResponseInterface');
$response->shouldReceive('getBody')
->andReturn('{"error": "invalid_grant", "error_description": "Code not found"}');
$response->shouldReceive('getHeader')
$response
->shouldReceive('getBody')
->andReturn($accessTokenResponseStream);
$response
->shouldReceive('getHeader')
->andReturn(['content-type' => 'json']);
$response
->shouldReceive('getStatusCode')
->andReturn(401);
$client = m::mock('GuzzleHttp\ClientInterface');
$client->shouldReceive('send')
$client
->shouldReceive('send')
->times(1)
->andReturn($response);
$this->provider->setHttpClient($client);
$token = $this->provider->getAccessToken('authorization_code', ['code' => 'mock_authorization_code']);
}
public function testCanDecryptResponseThrowsExceptionIfResponseIsNotAStringAndEncryptionIsNotUsed(): void
{
$this->expectException(EncryptionConfigurationException::class);
$this->provider->decryptResponse('');
$this->assertFalse($this->provider->usesEncryption());
}
public function testCanDecryptResponseReturnsResponseWhenEncryptionIsUsed(): void
{
$jwtPayload = json_decode(
sprintf(
$this->jwtTemplate,
(new DateTimeImmutable())->add(new DateInterval('PT1H'))->getTimestamp(),
(new DateTimeImmutable())->sub(new DateInterval('P1D'))->getTimestamp(),
(new DateTimeImmutable())->sub(new DateInterval('P1D'))->getTimestamp()
),
true
);
$jwt = JWT::encode(
$jwtPayload,
self::ENCRYPTION_KEY,
self::ENCRYPTION_ALGORITHM
);
$this->provider
->setEncryptionAlgorithm(self::ENCRYPTION_ALGORITHM)
->setEncryptionKey(self::ENCRYPTION_KEY);
$response = $this->provider->decryptResponse($jwt);
$this->assertSame($jwtPayload, $response);
}
}
}