Skip to content

Commit

Permalink
Merge pull request #40 from pdsinterop/feature/legacy-support
Browse files Browse the repository at this point in the history
add key ID to the ID token, used for non-dpop applications
  • Loading branch information
ylebre authored Jan 17, 2024
2 parents 60464a6 + 06767b4 commit 8ed8a4c
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 6 deletions.
2 changes: 1 addition & 1 deletion src/Config/KeysInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/TokenGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
144 changes: 144 additions & 0 deletions src/Utils/Bearer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
<?php declare(strict_types=1);

namespace Pdsinterop\Solid\Auth\Utils;

use DateInterval;
use Exception;
use Jose\Component\Core\JWK;
use Jose\Component\Core\Util\ECKey;
use Jose\Component\Core\Util\RSAKey;
use Lcobucci\Clock\SystemClock;
use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Token\InvalidTokenStructure;
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
use Lcobucci\JWT\Validation\Constraint\SignedWith;
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
use Pdsinterop\Solid\Auth\Exception\AuthorizationHeaderException;
use Pdsinterop\Solid\Auth\Exception\InvalidTokenException;
use Psr\Http\Message\ServerRequestInterface;

/**
* This class contains code to fetch the WebId from a request
* that is make in legacy mode (bearer token with pop)
*
* @ TODO: Make sure this code complies with the spec and validate the tokens properly;
* https://datatracker.ietf.org/doc/html/rfc7800
*/
class Bearer {

private JtiValidator $jtiValidator;

public function __construct(JtiValidator $jtiValidator)
{
$this->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');
}
}
}
9 changes: 4 additions & 5 deletions src/Utils/Jwks.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
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;

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;
}
Expand Down Expand Up @@ -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);

Expand Down
19 changes: 19 additions & 0 deletions tests/unit/TokenGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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');
Expand All @@ -323,6 +341,7 @@ final public function testIdTokenGenerationWithoutDpopKey(): void
[
'typ' => 'JWT',
'alg' => 'RS256',
'kid' => '0c3932ca20f3a00ad2eb72035f6cc9cb'
],
[
'at_hash' => '1EZBnvsFWlK8ESkgHQsrIQ',
Expand Down

0 comments on commit 8ed8a4c

Please sign in to comment.