Skip to content

Commit 6fa3462

Browse files
authored
Merge pull request #2 from pdsinterop/feature/jwks
Add JWKs request response.
2 parents ac91607 + e730a9c commit 6fa3462

9 files changed

Lines changed: 220 additions & 4 deletions

File tree

composer.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,13 @@
3939
"lcobucci/jwt": "^3.3",
4040
"phpunit/phpunit": "^8.5"
4141
},
42+
"scripts": {
43+
"tests:example": "php -S localhost:8080 -t ./tests/ ./tests/example.php",
44+
"tests:unit": "phpunit ./tests/unit"
45+
},
46+
"scripts-descriptions": {
47+
"tests:example": "Run internal PHP development server with example code",
48+
"tests:unit": "Run unit-test with PHPUnit"
49+
},
4250
"type": "library"
4351
}

src/Config/Keys.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Pdsinterop\Solid\Auth\Config;
44

55
use Defuse\Crypto\Key as CryptoKey;
6+
use Lcobucci\JWT\Signer\Key;
67
use League\OAuth2\Server\CryptKey;
78

89
class Keys
@@ -13,6 +14,8 @@ class Keys
1314
private $encryptionKey;
1415
/** @var CryptKey*/
1516
private $privateKey;
17+
/** @var Key */
18+
private $publicKey;
1619

1720
//////////////////////////// GETTERS AND SETTERS \\\\\\\\\\\\\\\\\\\\\\\\\\\
1821

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

34+
/*** @return Key */
35+
public function getPublicKey() : Key
36+
{
37+
return $this->publicKey;
38+
}
39+
3140
//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
3241

3342
/**
@@ -36,10 +45,11 @@ final public function getPrivateKey() : CryptKey
3645
* @param CryptKey $privateKey
3746
* @param string|CryptoKey $encryptionKey
3847
*/
39-
final public function __construct(CryptKey $privateKey, $encryptionKey)
48+
final public function __construct(CryptKey $privateKey, Key $publicKey, $encryptionKey)
4049
{
4150
// @FIXME: Add type-check for $encryptionKey (or an extending class with different parameter type?)
4251
$this->encryptionKey = $encryptionKey;
4352
$this->privateKey = $privateKey;
53+
$this->publicKey = $publicKey;
4454
}
4555
}

src/Enum/Jwk/Parameter.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Pdsinterop\Solid\Auth\Enum\Jwk;
4+
5+
class Parameter
6+
{
7+
public const ALGORITHM = 'alg';
8+
9+
public const KEY_ID = 'kid';
10+
11+
public const KEY_OPERATIONS = 'key_ops';
12+
13+
public const KEY_TYPE = 'kty';
14+
15+
public const PUBLIC_KEY_USE = 'use';
16+
17+
public const X_509_CERTIFICATE_CHAIN = 'x5c';
18+
19+
public const X_509_CERTIFICATE_SHA_1_THUMBPRINT = 'x5t';
20+
21+
public const X_509_CERTIFICATE_SHA_256_THUMBPRINT = 'x5t#S256';
22+
23+
public const X_509_URL = 'x5u';
24+
}

src/Enum/Rsa/Parameter.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace Pdsinterop\Solid\Auth\Enum\Rsa;
4+
5+
/**
6+
* Parameters for RSA Public Keys
7+
*
8+
* These members MUST be present for RSA public keys.
9+
*
10+
* The RSA Key blinding operation [Kocher], which is a defense against some
11+
* timing attacks, requires all of the RSA key values "n", "e", and "d".
12+
*
13+
* However, some RSA private key representations do not include the public
14+
* exponent "e", but only include the modulus "n" and the private exponent
15+
* "d". This is true, for instance, of the Java RSAPrivateKeySpec API, which
16+
* does not include the public exponent "e" as a parameter. So as to enable
17+
* RSA key blinding, such representations should be avoided. For Java, the
18+
* RSAPrivateCrtKeySpec API can be used instead. Section 8.2.2(i) of the
19+
* "Handbook of Applied Cryptography" [HAC] discusses how to compute the
20+
* remaining RSA private key parameters, if needed, using only "n", "e",
21+
* and "d".}
22+
*
23+
* @see https://tools.ietf.org/html/rfc7518#section-6.3
24+
*/
25+
class Parameter
26+
{
27+
/**
28+
* The "e" (exponent) parameter contains the exponent value for the RSA
29+
* public key. It is represented as a Base64urlUInt-encoded value. For
30+
* instance, when representing the value 65537, the octet sequence to be
31+
* base64url-encoded MUST consist of the three octets [1, 0, 1]; the
32+
* resulting representation for this value is "AQAB".
33+
*/
34+
public const PUBLIC_EXPONENT = 'e';
35+
36+
/**
37+
* The "n" (modulus) parameter contains the modulus value for the RSA public
38+
* key. It is represented as a Base64urlUInt-encoded value.
39+
*/
40+
public const PUBLIC_MODULUS = 'n';
41+
42+
/**
43+
* The "d" (private exponent) parameter contains the private exponent value
44+
* for the RSA private key. It is represented as a Base64urlUInt-encoded
45+
* value.}
46+
*/
47+
public const PRIVATE_EXPONENT = 'd';
48+
}

src/Factory/ConfigFactory.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Pdsinterop\Solid\Auth\Factory;
44

5+
use Lcobucci\JWT\Signer\Key;
56
use League\OAuth2\Server\CryptKey;
67
use Pdsinterop\Solid\Auth\Config;
78
use Pdsinterop\Solid\Auth\Enum\OAuth2\GrantType;
@@ -17,6 +18,8 @@ class ConfigFactory
1718
private $encryptionKey;
1819
/** @var string */
1920
private $privateKey;
21+
/** @var string */
22+
private $publicKey;
2023
/** @var array */
2124
private $serverConfig;
2225

@@ -25,13 +28,15 @@ final public function __construct(
2528
string $clientSecret,
2629
string $encryptionKey,
2730
string $privateKey,
31+
string $publicKey,
2832
array $serverConfig
2933
) {
3034
$this->clientIdentifier = $clientIdentifier;
3135
$this->clientSecret = $clientSecret;
3236
$this->encryptionKey = $encryptionKey;
3337
$this->privateKey = $privateKey;
3438
$this->serverConfig = $serverConfig;
39+
$this->publicKey = $publicKey;
3540
}
3641

3742
final public function create() : Config
@@ -40,6 +45,7 @@ final public function create() : Config
4045
$clientSecret = $this->clientSecret;
4146
$encryptionKey = $this->encryptionKey;
4247
$privateKey = $this->privateKey;
48+
$publicKey = $this->publicKey;
4349

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

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

5460
$keys = new Config\Keys(
5561
new CryptKey($privateKey),
62+
new Key($publicKey),
5663
$encryptionKey
5764
);
5865

src/Server.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use League\OAuth2\Server\Exception\OAuthServerException;
88
use Pdsinterop\Solid\Auth\Entity\User;
99
use Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata as OidcMeta;
10+
use Pdsinterop\Solid\Auth\Utils\Jwks;
1011
use Psr\Http\Message\ResponseInterface as Response;
1112
use Psr\Http\Message\ServerRequestInterface as Request;
1213

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

58+
final public function respondToJwksRequest(/*Jwks $jwks*/) : Response
59+
{
60+
$response = $this->response;
61+
$key = $this->config->getKeys()->getPublicKey();
62+
63+
$jwks = new Jwks($key);
64+
65+
return $this->createJsonResponse($response, $jwks);
66+
}
67+
5768
final public function respondToAuthorizationRequest(
5869
Request $request,
5970
User $user = null,

src/Utils/Base64Url.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Pdsinterop\Solid\Auth\Utils;
4+
5+
/**
6+
* URL-safe Base64 encode and decode
7+
*
8+
* ...as PHP does not natively offer this functionality
9+
*/
10+
class Base64Url
11+
{
12+
private const URL_UNSAFE = '+/';
13+
private const URL_SAFE = '-_';
14+
15+
public static function encode($subject) : string
16+
{
17+
return strtr(rtrim(base64_encode($subject), '='), self::URL_UNSAFE, self::URL_SAFE);
18+
}
19+
20+
public static function decode($subject) : string
21+
{
22+
return base64_decode(strtr($subject, self::URL_SAFE, self::URL_UNSAFE));
23+
}
24+
}

src/Utils/Jwks.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace Pdsinterop\Solid\Auth\Utils;
4+
5+
use JsonSerializable;
6+
use Lcobucci\JWT\Signer\Key;
7+
use Pdsinterop\Solid\Auth\Enum\Jwk\Parameter as JwkParameter;
8+
use Pdsinterop\Solid\Auth\Enum\Rsa\Parameter as RsaParameter;
9+
10+
class Jwks implements JsonSerializable
11+
{
12+
////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\
13+
14+
/** @var Key */
15+
private $publicKey;
16+
17+
//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
18+
19+
final public function __construct(Key $publicKey)
20+
{
21+
$this->publicKey = $publicKey;
22+
}
23+
24+
final public function __toString() : string
25+
{
26+
return (string) json_encode($this);
27+
}
28+
29+
final public function jsonSerialize()
30+
{
31+
return $this->create();
32+
}
33+
34+
////////////////////////////// UTILITY METHODS \\\\\\\\\\\\\\\\\\\\\\\\\\\\\
35+
36+
/**
37+
* @param string $certificate
38+
* @param $subject
39+
*
40+
* @return array
41+
*/
42+
private function createKey(string $certificate, $subject) : array
43+
{
44+
return [
45+
JwkParameter::ALGORITHM => 'RS256',
46+
JwkParameter::KEY_ID => md5($certificate),
47+
JwkParameter::KEY_TYPE => 'RSA',
48+
RsaParameter::PUBLIC_EXPONENT => 'AQAB', // Hard-coded as `Base64Url::encode($keyInfo['rsa']['e'])` tends to be empty...
49+
RsaParameter::PUBLIC_MODULUS => Base64Url::encode($subject),
50+
];
51+
}
52+
53+
/**
54+
* As the JWT library does not (yet?) have support for JWK, a custom solution is used for now.
55+
*
56+
* @return array
57+
*
58+
* @see https://github.com/lcobucci/jwt/issues/32
59+
*/
60+
private function create() : array
61+
{
62+
$jwks = ['keys' => []];
63+
64+
$publicKeys = [$this->publicKey];
65+
66+
array_walk($publicKeys, function (Key $publicKey) use (&$jwks) {
67+
$certificate = $publicKey->getContent();
68+
69+
$key = openssl_pkey_get_public($certificate);
70+
$keyInfo = openssl_pkey_get_details($key);
71+
72+
$jwks['keys'][] = $this->createKey($certificate, $keyInfo['rsa']['n']);
73+
});
74+
75+
return $jwks;
76+
}
77+
}

tests/example.php

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@
3434
$keyPath = dirname(__DIR__) . '/tests/fixtures/keys';
3535
$encryptionKey = file_get_contents($keyPath . '/encryption.key');
3636
$privateKey = file_get_contents($keyPath . '/private.key');
37+
$publicKey = file_get_contents($keyPath . '/public.key');
3738

3839
$config = (new \Pdsinterop\Solid\Auth\Factory\ConfigFactory(
3940
$clientIdentifier,
4041
$clientSecret,
4142
$encryptionKey,
4243
$privateKey,
44+
$publicKey,
4345
[
4446
/* URL of the OP's OAuth 2.0 Authorization Endpoint [OpenID.Core]. */
4547
\Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::AUTHORIZATION_ENDPOINT => 'https://server/authorize',
@@ -67,7 +69,7 @@
6769
* of keys provided. When used, the bare key values MUST still be
6870
* present and MUST match those in the certificate.
6971
*/
70-
\Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::JWKS_URI => 'https://server/jwk'
72+
\Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata::JWKS_URI => 'https://server/.well-known/jwks.json'
7173
]
7274
))->create();
7375

@@ -86,7 +88,7 @@
8688
// =============================================================================
8789
// Handle requests
8890
// -----------------------------------------------------------------------------
89-
switch ($request->getMethod() . $request->getUri()) {
91+
switch ($request->getMethod() . $request->getRequestTarget()) {
9092
// @CHECKME: Do we also need 'GET/.well-known/oauth-authorization-server'?
9193
case 'GET/.well-known/openid-configuration':
9294
$response = $server->respondToWellKnownRequest();
@@ -177,7 +179,12 @@
177179
$response = $server->respondToAuthorizationRequest($request, $user, $approval, $callback);
178180
break;
179181

182+
case 'GET/.well-known/jwks.json':
183+
$response = $server->respondToJwksRequest();
184+
break;
185+
180186
default:
187+
$response->getBody()->write('404');
181188
$response = $response->withStatus(404);
182189
break;
183190
}
@@ -193,6 +200,6 @@
193200
}
194201
}
195202

196-
echo $response->getBody()->getContents();
203+
echo (string) $response->getBody();
197204
exit;
198205
// =============================================================================

0 commit comments

Comments
 (0)