Skip to content

Commit

Permalink
Merge pull request #2 from pdsinterop/feature/jwks
Browse files Browse the repository at this point in the history
Add JWKs request response.
  • Loading branch information
Potherca authored Sep 15, 2020
2 parents ac91607 + e730a9c commit 6fa3462
Show file tree
Hide file tree
Showing 9 changed files with 220 additions and 4 deletions.
8 changes: 8 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,13 @@
"lcobucci/jwt": "^3.3",
"phpunit/phpunit": "^8.5"
},
"scripts": {
"tests:example": "php -S localhost:8080 -t ./tests/ ./tests/example.php",
"tests:unit": "phpunit ./tests/unit"
},
"scripts-descriptions": {
"tests:example": "Run internal PHP development server with example code",
"tests:unit": "Run unit-test with PHPUnit"
},
"type": "library"
}
12 changes: 11 additions & 1 deletion src/Config/Keys.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Pdsinterop\Solid\Auth\Config;

use Defuse\Crypto\Key as CryptoKey;
use Lcobucci\JWT\Signer\Key;
use League\OAuth2\Server\CryptKey;

class Keys
Expand All @@ -13,6 +14,8 @@ class Keys
private $encryptionKey;
/** @var CryptKey*/
private $privateKey;
/** @var Key */
private $publicKey;

//////////////////////////// GETTERS AND SETTERS \\\\\\\\\\\\\\\\\\\\\\\\\\\

Expand All @@ -28,6 +31,12 @@ final public function getPrivateKey() : CryptKey
return $this->privateKey;
}

/*** @return Key */
public function getPublicKey() : Key
{
return $this->publicKey;
}

//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

/**
Expand All @@ -36,10 +45,11 @@ final public function getPrivateKey() : CryptKey
* @param CryptKey $privateKey
* @param string|CryptoKey $encryptionKey
*/
final public function __construct(CryptKey $privateKey, $encryptionKey)
final public function __construct(CryptKey $privateKey, Key $publicKey, $encryptionKey)
{
// @FIXME: Add type-check for $encryptionKey (or an extending class with different parameter type?)
$this->encryptionKey = $encryptionKey;
$this->privateKey = $privateKey;
$this->publicKey = $publicKey;
}
}
24 changes: 24 additions & 0 deletions src/Enum/Jwk/Parameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Pdsinterop\Solid\Auth\Enum\Jwk;

class Parameter
{
public const ALGORITHM = 'alg';

public const KEY_ID = 'kid';

public const KEY_OPERATIONS = 'key_ops';

public const KEY_TYPE = 'kty';

public const PUBLIC_KEY_USE = 'use';

public const X_509_CERTIFICATE_CHAIN = 'x5c';

public const X_509_CERTIFICATE_SHA_1_THUMBPRINT = 'x5t';

public const X_509_CERTIFICATE_SHA_256_THUMBPRINT = 'x5t#S256';

public const X_509_URL = 'x5u';
}
48 changes: 48 additions & 0 deletions src/Enum/Rsa/Parameter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace Pdsinterop\Solid\Auth\Enum\Rsa;

/**
* Parameters for RSA Public Keys
*
* These members MUST be present for RSA public keys.
*
* The RSA Key blinding operation [Kocher], which is a defense against some
* timing attacks, requires all of the RSA key values "n", "e", and "d".
*
* However, some RSA private key representations do not include the public
* exponent "e", but only include the modulus "n" and the private exponent
* "d". This is true, for instance, of the Java RSAPrivateKeySpec API, which
* does not include the public exponent "e" as a parameter. So as to enable
* RSA key blinding, such representations should be avoided. For Java, the
* RSAPrivateCrtKeySpec API can be used instead. Section 8.2.2(i) of the
* "Handbook of Applied Cryptography" [HAC] discusses how to compute the
* remaining RSA private key parameters, if needed, using only "n", "e",
* and "d".}
*
* @see https://tools.ietf.org/html/rfc7518#section-6.3
*/
class Parameter
{
/**
* The "e" (exponent) parameter contains the exponent value for the RSA
* public key. It is represented as a Base64urlUInt-encoded value. For
* instance, when representing the value 65537, the octet sequence to be
* base64url-encoded MUST consist of the three octets [1, 0, 1]; the
* resulting representation for this value is "AQAB".
*/
public const PUBLIC_EXPONENT = 'e';

/**
* The "n" (modulus) parameter contains the modulus value for the RSA public
* key. It is represented as a Base64urlUInt-encoded value.
*/
public const PUBLIC_MODULUS = 'n';

/**
* The "d" (private exponent) parameter contains the private exponent value
* for the RSA private key. It is represented as a Base64urlUInt-encoded
* value.}
*/
public const PRIVATE_EXPONENT = 'd';
}
7 changes: 7 additions & 0 deletions src/Factory/ConfigFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Pdsinterop\Solid\Auth\Factory;

use Lcobucci\JWT\Signer\Key;
use League\OAuth2\Server\CryptKey;
use Pdsinterop\Solid\Auth\Config;
use Pdsinterop\Solid\Auth\Enum\OAuth2\GrantType;
Expand All @@ -17,6 +18,8 @@ class ConfigFactory
private $encryptionKey;
/** @var string */
private $privateKey;
/** @var string */
private $publicKey;
/** @var array */
private $serverConfig;

Expand All @@ -25,13 +28,15 @@ final public function __construct(
string $clientSecret,
string $encryptionKey,
string $privateKey,
string $publicKey,
array $serverConfig
) {
$this->clientIdentifier = $clientIdentifier;
$this->clientSecret = $clientSecret;
$this->encryptionKey = $encryptionKey;
$this->privateKey = $privateKey;
$this->serverConfig = $serverConfig;
$this->publicKey = $publicKey;
}

final public function create() : Config
Expand All @@ -40,6 +45,7 @@ final public function create() : Config
$clientSecret = $this->clientSecret;
$encryptionKey = $this->encryptionKey;
$privateKey = $this->privateKey;
$publicKey = $this->publicKey;

$client = new Config\Client($clientIdentifier, $clientSecret);

Expand All @@ -53,6 +59,7 @@ final public function create() : Config

$keys = new Config\Keys(
new CryptKey($privateKey),
new Key($publicKey),
$encryptionKey
);

Expand Down
11 changes: 11 additions & 0 deletions src/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use League\OAuth2\Server\Exception\OAuthServerException;
use Pdsinterop\Solid\Auth\Entity\User;
use Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata as OidcMeta;
use Pdsinterop\Solid\Auth\Utils\Jwks;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;

Expand Down Expand Up @@ -54,6 +55,16 @@ final public function respondToWellKnownRequest() : Response
return $this->createJsonResponse($response, $serverConfig);
}

final public function respondToJwksRequest(/*Jwks $jwks*/) : Response
{
$response = $this->response;
$key = $this->config->getKeys()->getPublicKey();

$jwks = new Jwks($key);

return $this->createJsonResponse($response, $jwks);
}

final public function respondToAuthorizationRequest(
Request $request,
User $user = null,
Expand Down
24 changes: 24 additions & 0 deletions src/Utils/Base64Url.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Pdsinterop\Solid\Auth\Utils;

/**
* URL-safe Base64 encode and decode
*
* ...as PHP does not natively offer this functionality
*/
class Base64Url
{
private const URL_UNSAFE = '+/';
private const URL_SAFE = '-_';

public static function encode($subject) : string
{
return strtr(rtrim(base64_encode($subject), '='), self::URL_UNSAFE, self::URL_SAFE);
}

public static function decode($subject) : string
{
return base64_decode(strtr($subject, self::URL_SAFE, self::URL_UNSAFE));
}
}
77 changes: 77 additions & 0 deletions src/Utils/Jwks.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

namespace Pdsinterop\Solid\Auth\Utils;

use JsonSerializable;
use Lcobucci\JWT\Signer\Key;
use Pdsinterop\Solid\Auth\Enum\Jwk\Parameter as JwkParameter;
use Pdsinterop\Solid\Auth\Enum\Rsa\Parameter as RsaParameter;

class Jwks implements JsonSerializable
{
////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\

/** @var Key */
private $publicKey;

//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

final public function __construct(Key $publicKey)
{
$this->publicKey = $publicKey;
}

final public function __toString() : string
{
return (string) json_encode($this);
}

final public function jsonSerialize()
{
return $this->create();
}

////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\

/**
* @param string $certificate
* @param $subject
*
* @return array
*/
private function createKey(string $certificate, $subject) : array
{
return [
JwkParameter::ALGORITHM => 'RS256',
JwkParameter::KEY_ID => md5($certificate),
JwkParameter::KEY_TYPE => 'RSA',
RsaParameter::PUBLIC_EXPONENT => 'AQAB', // Hard-coded as `Base64Url::encode($keyInfo['rsa']['e'])` tends to be empty...
RsaParameter::PUBLIC_MODULUS => Base64Url::encode($subject),
];
}

/**
* As the JWT library does not (yet?) have support for JWK, a custom solution is used for now.
*
* @return array
*
* @see https://github.com/lcobucci/jwt/issues/32
*/
private function create() : array
{
$jwks = ['keys' => []];

$publicKeys = [$this->publicKey];

array_walk($publicKeys, function (Key $publicKey) use (&$jwks) {
$certificate = $publicKey->getContent();

$key = openssl_pkey_get_public($certificate);
$keyInfo = openssl_pkey_get_details($key);

$jwks['keys'][] = $this->createKey($certificate, $keyInfo['rsa']['n']);
});

return $jwks;
}
}
13 changes: 10 additions & 3 deletions tests/example.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,14 @@
$keyPath = dirname(__DIR__) . '/tests/fixtures/keys';
$encryptionKey = file_get_contents($keyPath . '/encryption.key');
$privateKey = file_get_contents($keyPath . '/private.key');
$publicKey = file_get_contents($keyPath . '/public.key');

$config = (new \Pdsinterop\Solid\Auth\Factory\ConfigFactory(
$clientIdentifier,
$clientSecret,
$encryptionKey,
$privateKey,
$publicKey,
[
/* URL of the OP's OAuth 2.0 Authorization Endpoint [OpenID.Core]. */
\Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::AUTHORIZATION_ENDPOINT => 'https://server/authorize',
Expand Down Expand Up @@ -67,7 +69,7 @@
* of keys provided. When used, the bare key values MUST still be
* present and MUST match those in the certificate.
*/
\Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::JWKS_URI => 'https://server/jwk'
\Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::JWKS_URI => 'https://server/.well-known/jwks.json'
]
))->create();

Expand All @@ -86,7 +88,7 @@
// =============================================================================
// Handle requests
// -----------------------------------------------------------------------------
switch ($request->getMethod() . $request->getUri()) {
switch ($request->getMethod() . $request->getRequestTarget()) {
// @CHECKME: Do we also need 'GET/.well-known/oauth-authorization-server'?
case 'GET/.well-known/openid-configuration':
$response = $server->respondToWellKnownRequest();
Expand Down Expand Up @@ -177,7 +179,12 @@
$response = $server->respondToAuthorizationRequest($request, $user, $approval, $callback);
break;

case 'GET/.well-known/jwks.json':
$response = $server->respondToJwksRequest();
break;

default:
$response->getBody()->write('404');
$response = $response->withStatus(404);
break;
}
Expand All @@ -193,6 +200,6 @@
}
}

echo $response->getBody()->getContents();
echo (string) $response->getBody();
exit;
// =============================================================================

0 comments on commit 6fa3462

Please sign in to comment.