Skip to content

Commit

Permalink
Add additional support for jwt encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenmaguire committed Dec 9, 2016
1 parent 0502481 commit ab916ed
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 118 deletions.
4 changes: 2 additions & 2 deletions .scrutinizer.yml
Expand Up @@ -29,7 +29,7 @@ tools:
paths: ['src']
php_loc:
enabled: true
excluded_dirs: [vendor, test]
excluded_dirs: [examples, vendor, test]
php_cpd:
enabled: true
excluded_dirs: [vendor, test]
excluded_dirs: [examples, vendor, test]
17 changes: 17 additions & 0 deletions CHANGELOG.md
@@ -1,6 +1,23 @@
# Changelog
All Notable changes to `oauth2-keycloak` will be documented in this file

## 0.2.0 - 2016-12-07

### Added
- JSON Web Token decryption support

### Deprecated
- Nothing

### Fixed
- Nothing

### Removed
- Nothing

### Security
- Nothing

## 0.1.0 - 2015-08-31

### Added
Expand Down
70 changes: 65 additions & 5 deletions README.md
Expand Up @@ -28,11 +28,14 @@ Use `realm` to specify the Keycloak realm name. You can lookup the correct value

```php
$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'authServerUrl' => '{keycloak-server-url}',
'realm' => '{keycloak-realm}',
'clientId' => '{keycloak-client-id}',
'clientSecret' => '{keycloak-client-secret}',
'redirectUri' => 'https://example.com/callback-url',
'authServerUrl' => '{keycloak-server-url}',
'realm' => '{keycloak-realm}',
'clientId' => '{keycloak-client-id}',
'clientSecret' => '{keycloak-client-secret}',
'redirectUri' => 'https://example.com/callback-url',
'encryptionAlgorithm' => 'RS256', // optional
'encryptionKeyPath' => '../key.pem' // optional
'encryptionKey' => 'contents_of_key_or_certificate' // optional
]);

if (!isset($_GET['code'])) {
Expand Down Expand Up @@ -92,6 +95,63 @@ $provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
$token = $provider->getAccessToken('refresh_token', ['refresh_token' => $token->getRefreshToken()]);
```

### Handling encryption

If you've configured your Keycloak instance to use encryption, there are some advanced options available to you.

#### Configure the provider to use the same encryption algorithm

```php
$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
// ...
'encryptionAlgorithm' => 'RS256',
]);
```

or

```php
$provider->setEncryptionAlgorithm('RS256');
```

#### Configure the provider to use the expected decryption public key or certificate

##### By key value

```php
$key = "-----BEGIN PUBLIC KEY-----\n....\n-----END PUBLIC KEY-----";
// or
// $key = "-----BEGIN CERTIFICATE-----\n....\n-----END CERTIFICATE-----";

$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
// ...
'encryptionKey' => $key,
]);
```

or

```php
$provider->setEncryptionKey($key);
```

##### By key path

```php
$keyPath = '../key.pem';

$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
// ...
'encryptionKeyPath' => $keyPath,
]);
```

or

```php
$provider->setEncryptionKeyPath($keyPath);
```

## Testing

``` bash
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -19,7 +19,8 @@
],
"require": {
"php": ">=5.5.0",
"league/oauth2-client": "~1.0"
"league/oauth2-client": "~1.0",
"firebase/php-jwt": "^4.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0",
Expand Down
53 changes: 53 additions & 0 deletions examples/index.php
@@ -0,0 +1,53 @@
<?php

require 'vendor/autoload.php';

session_start();

$provider = new Stevenmaguire\OAuth2\Client\Provider\Keycloak([
'authServerUrl' => '',
'realm' => '',
'clientId' => '',
'clientSecret' => '',
'redirectUri' => '',
'encryptionAlgorithm' => null,
'encryptionKey' => null,
'encryptionKeyPath' => null
]);

if (!isset($_GET['code'])) {
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
header('Location: '.$authUrl);
exit;

// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
unset($_SESSION['oauth2state']);
exit('Invalid state, make sure HTTP sessions are enabled.');
} else {
// Try to get an access token (using the authorization coe grant)
try {
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
} catch (Exception $e) {
exit('Failed to get access token: '.$e->getMessage());
}

// Optional: Now you have a token you can look up a users profile data
try {

// We got an access token, let's now get the user's details
$user = $provider->getResourceOwner($token);
// Use these details to create a new profile
printf('Hello %s!\n<br>', $user->getName());

} catch (Exception $e) {
exit('Failed to get resource owner: '.$e->getMessage());
}

// Use this to interact with an API on the users behalf
echo $token->getToken();
}
1 change: 1 addition & 0 deletions phpunit.xml.dist
Expand Up @@ -29,6 +29,7 @@
<whitelist>
<directory suffix=".php">./</directory>
<exclude>
<directory suffix=".php">./examples</directory>
<directory suffix=".php">./vendor</directory>
<directory suffix=".php">./test</directory>
</exclude>
Expand Down
10 changes: 10 additions & 0 deletions src/Provider/Exception/EncryptionConfigurationException.php
@@ -0,0 +1,10 @@
<?php

namespace Stevenmaguire\OAuth2\Client\Provider\Exception;

use Exception;

class EncryptionConfigurationException extends Exception
{
// nothing special here, just a name
}
148 changes: 146 additions & 2 deletions src/Provider/Keycloak.php
Expand Up @@ -2,11 +2,14 @@

namespace Stevenmaguire\OAuth2\Client\Provider;

use Exception;
use Firebase\JWT\JWT;
use League\OAuth2\Client\Provider\AbstractProvider;
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
use League\OAuth2\Client\Token\AccessToken;
use League\OAuth2\Client\Tool\BearerAuthorizationTrait;
use Psr\Http\Message\ResponseInterface;
use Stevenmaguire\OAuth2\Client\Provider\Exception\EncryptionConfigurationException;

class Keycloak extends AbstractProvider
{
Expand All @@ -26,6 +29,85 @@ class Keycloak extends AbstractProvider
*/
public $realm = null;

/**
* Encryption algorithm.
*
* You must specify supported algorithms for your application. See
* https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
* for a list of spec-compliant algorithms.
*
* @var string
*/
public $encryptionAlgorithm = null;

/**
* Encryption key.
*
* @var string
*/
public $encryptionKey = null;

/**
* Constructs an OAuth 2.0 service provider.
*
* @param array $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
* 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'])) {
$this->setEncryptionKeyPath($options['encryptionKeyPath']);
unset($options['encryptionKeyPath']);
}
parent::__construct($options, $collaborators);
}

/**
* Attempts to decrypt the given response.
*
* @param string|array|null $response
*
* @return string|array|null
*/
public function decryptResponse($response)
{
if (is_string($response)) {
if ($this->encryptionAlgorithm && $this->encryptionKey) {
try {
$response = json_decode(
json_encode(
JWT::decode(
$response,
$this->encryptionKey,
array($this->encryptionAlgorithm)
)
),
true
);
} catch (Exception $e) {
throw new EncryptionConfigurationException(
$e->getMessage(),
400,
$e
);
}
} else {
throw new EncryptionConfigurationException(
'The given response may be encrypted and sufficient '.
'encryption configuration has not been provided.',
400
);
}
}

return $response;
}

/**
* Get authorization url to begin OAuth flow
*
Expand Down Expand Up @@ -76,7 +158,7 @@ protected function getBaseUrlWithRealm()
* This should not be a complete list of all scopes, but the minimum
* required for the provider user interface!
*
* @return array
* @return string[]
*/
protected function getDefaultScopes()
{
Expand Down Expand Up @@ -104,10 +186,72 @@ protected function checkResponse(ResponseInterface $response, $data)
*
* @param array $response
* @param AccessToken $token
* @return League\OAuth2\Client\Provider\ResourceOwnerInterface
* @return KeycloakResourceOwner
*/
protected function createResourceOwner(array $response, AccessToken $token)
{
return new KeycloakResourceOwner($response);
}

/**
* Requests and returns the resource owner of given access token.
*
* @param AccessToken $token
* @return KeycloakResourceOwner
*/
public function getResourceOwner(AccessToken $token)
{
$response = $this->fetchResourceOwnerDetails($token);

$response = $this->decryptResponse($response);

return $this->createResourceOwner($response, $token);
}

/**
* Updates expected encryption algorithm of Keycloak instance.
*
* @param string $encryptionAlgorithm
*
* @return Keycloak
*/
public function setEncryptionAlgorithm($encryptionAlgorithm)
{
$this->encryptionAlgorithm = $encryptionAlgorithm;

return $this;
}

/**
* Updates expected encryption key of Keycloak instance.
*
* @param string $encryptionKey
*
* @return Keycloak
*/
public function setEncryptionKey($encryptionKey)
{
$this->encryptionKey = $encryptionKey;

return $this;
}

/**
* Updates expected encryption key of Keycloak instance to content of given
* file path.
*
* @param string $encryptionKeyPath
*
* @return Keycloak
*/
public function setEncryptionKeyPath($encryptionKeyPath)
{
try {
$this->encryptionKey = file_get_contents($encryptionKeyPath);
} catch (Exception $e) {
// Not sure how to handle this yet.
}

return $this;
}
}

0 comments on commit ab916ed

Please sign in to comment.