diff --git a/src/Config/KeysInterface.php b/src/Config/KeysInterface.php index 395b112..b22dbe6 100644 --- a/src/Config/KeysInterface.php +++ b/src/Config/KeysInterface.php @@ -3,7 +3,7 @@ namespace Pdsinterop\Solid\Auth\Config; use Defuse\Crypto\Key as CryptoKey; -use Lcobucci\JWT\Signer\Key\InMemory as Key; +use Lcobucci\JWT\Signer\Key as Key; use League\OAuth2\Server\CryptKey; interface KeysInterface diff --git a/src/TokenGenerator.php b/src/TokenGenerator.php index ee48cff..e6fc26f 100644 --- a/src/TokenGenerator.php +++ b/src/TokenGenerator.php @@ -4,6 +4,7 @@ use Pdsinterop\Solid\Auth\Exception\InvalidTokenException; use Pdsinterop\Solid\Auth\Utils\DPop; +use Pdsinterop\Solid\Auth\Utils\Jwks; use Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata as OidcMeta; use Laminas\Diactoros\Response\JsonResponse; use League\OAuth2\Server\CryptTrait; @@ -88,6 +89,10 @@ public function generateIdToken($accessToken, $clientId, $subject, $nonce, $priv $token = $token->withClaim("cnf", [ "jkt" => $jkt, ]); + } else { + // legacy mode + $jwks = $this->getJwks(); + $token = $token->withHeader('kid', $jwks['keys'][0]['kid']); } return $token->getToken($jwtConfig->signer(), $jwtConfig->signingKey())->toString(); @@ -201,4 +206,10 @@ private function makeJwkThumbprint($dpop): string return $this->dpopUtil->makeJwkThumbprint($jwk); } + + private function getJwks() { + $key = $this->config->getKeys()->getPublicKey(); + $jwks = new Jwks($key); + return json_decode((string) $jwks, true); + } } diff --git a/src/Utils/Bearer.php b/src/Utils/Bearer.php new file mode 100644 index 0000000..09cded0 --- /dev/null +++ b/src/Utils/Bearer.php @@ -0,0 +1,144 @@ +jtiValidator = $jtiValidator; + } + + /** + * This method fetches the WebId from a request and verifies + * that the request has a valid pop token that matches + * the access token. + * + * @param ServerRequestInterface $request Server Request + * + * @return string the WebId, or "public" if no WebId is found + * + * @throws Exception "Invalid token" when the pop token is invalid + */ + public function getWebId($request) { + $serverParams = $request->getServerParams(); + + if (empty($serverParams['HTTP_AUTHORIZATION'])) { + $webId = "public"; + } else { + $this->validateRequestHeaders($serverParams); + + [, $jwt] = explode(" ", $serverParams['HTTP_AUTHORIZATION'], 2); + + try { + $this->validateJwt($jwt, $request); + } catch (RequiredConstraintsViolated $e) { + throw new InvalidTokenException($e->getMessage(), 0, $e); + } + $idToken = $this->getIdTokenFromJwt($jwt); + + try { + $this->validateIdToken($idToken, $request); + } catch (RequiredConstraintsViolated $e) { + throw new InvalidTokenException($e->getMessage(), 0, $e); + } + $webId = $this->getSubjectFromIdToken($idToken); + } + + return $webId; + } + + /** + * @param string $jwt JWT access token, raw + * @param ServerRequestInterface $request Server Request + * @return bool + * + * FIXME: Add more validations to the token; + */ + public function validateJwt($jwt, $request) { + $jwtConfig = Configuration::forUnsecuredSigner(); + $jwtConfig->parser()->parse($jwt); + return true; + } + + /** + * validates that the provided OIDC ID Token + * @param string $token The OIDS ID Token (raw) + * @param ServerRequestInterface $request Server Request + * @return bool True if the id token is valid + * @throws InvalidTokenException when the tokens is not valid + * + * FIXME: Add more validations to the token; + */ + public function validateIdToken($token, $request) { + $jwtConfig = Configuration::forUnsecuredSigner(); + $jwtConfig->parser()->parse($token); + return true; + } + + private function getIdTokenFromJwt($jwt) { + $jwtConfig = Configuration::forUnsecuredSigner(); + try { + $jwt = $jwtConfig->parser()->parse($jwt); + } catch(Exception $e) { + throw new InvalidTokenException("Invalid JWT token", 409, $e); + } + + $idToken = $jwt->claims()->get("id_token"); + if ($idToken === null) { + throw new InvalidTokenException('Missing "id_token"'); + } + return $idToken; + } + + private function getSubjectFromIdToken($idToken) { + $jwtConfig = Configuration::forUnsecuredSigner(); + try { + $jwt = $jwtConfig->parser()->parse($idToken); + } catch(Exception $e) { + throw new InvalidTokenException("Invalid ID token", 409, $e); + } + + $sub = $jwt->claims()->get("sub"); + if ($sub === null) { + throw new InvalidTokenException('Missing "sub"'); + } + return $sub; + } + + private function validateRequestHeaders($serverParams) { + if (str_contains($serverParams['HTTP_AUTHORIZATION'], ' ') === false) { + throw new AuthorizationHeaderException("Authorization Header does not contain parameters"); + } + + if (str_starts_with(strtolower($serverParams['HTTP_AUTHORIZATION']), 'bearer') === false) { + throw new AuthorizationHeaderException('Only "bearer" authorization scheme is supported'); + } + } +} diff --git a/src/Utils/Jwks.php b/src/Utils/Jwks.php index ea33ac8..8bc3095 100644 --- a/src/Utils/Jwks.php +++ b/src/Utils/Jwks.php @@ -3,7 +3,7 @@ namespace Pdsinterop\Solid\Auth\Utils; use JsonSerializable; -use Lcobucci\JWT\Signer\Key\InMemory; +use Lcobucci\JWT\Signer\Key as Key; use Pdsinterop\Solid\Auth\Enum\Jwk\Parameter as JwkParameter; use Pdsinterop\Solid\Auth\Enum\Rsa\Parameter as RsaParameter; @@ -11,12 +11,12 @@ class Jwks implements JsonSerializable { ////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\ - /** @var InMemory */ + /** @var Key */ private $publicKey; //////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ - final public function __construct(InMemory $publicKey) + final public function __construct(Key $publicKey) { $this->publicKey = $publicKey; } @@ -64,9 +64,8 @@ private function create() : array $publicKeys = [$this->publicKey]; - array_walk($publicKeys, function (InMemory $publicKey) use (&$jwks) { + array_walk($publicKeys, function (Key $publicKey) use (&$jwks) { $certificate = $publicKey->contents(); - $key = openssl_pkey_get_public($certificate); $keyInfo = openssl_pkey_get_details($key); diff --git a/tests/unit/TokenGeneratorTest.php b/tests/unit/TokenGeneratorTest.php index 17a01c7..bcbba41 100644 --- a/tests/unit/TokenGeneratorTest.php +++ b/tests/unit/TokenGeneratorTest.php @@ -281,6 +281,8 @@ final public function testIdTokenGenerationWithoutPrivateKey(): void * @testdox Token Generator SHOULD generate a token without Confirmation JWT Thumbprint (CNF JKT) WHEN asked to generate a IdToken without dpopKey * * @covers ::generateIdToken + * + * @uses \Pdsinterop\Solid\Auth\Utils\Jwks */ final public function testIdTokenGenerationWithoutDpopKey(): void { @@ -305,6 +307,22 @@ final public function testIdTokenGenerationWithoutDpopKey(): void ->willReturn('mock issuer') ; + $publicKey = file_get_contents(__DIR__.'/../fixtures/keys/public.key'); + + $mockPublicKey = $this->getMockBuilder(\Lcobucci\JWT\Signer\Key::class) + ->getMock() + ; + + $mockPublicKey->expects($this->once()) + ->method('contents') + ->willReturn($publicKey) + ; + + $this->mockKeys->expects($this->once()) + ->method('getPublicKey') + ->willReturn($mockPublicKey) + ; + $privateKey = file_get_contents(__DIR__.'/../fixtures/keys/private.key'); $now = new \DateTimeImmutable('1234-01-01 12:34:56.789'); @@ -323,6 +341,7 @@ final public function testIdTokenGenerationWithoutDpopKey(): void [ 'typ' => 'JWT', 'alg' => 'RS256', + 'kid' => '0c3932ca20f3a00ad2eb72035f6cc9cb' ], [ 'at_hash' => '1EZBnvsFWlK8ESkgHQsrIQ',