Skip to content

Commit 8ed8a4c

Browse files
authored
Merge pull request #40 from pdsinterop/feature/legacy-support
add key ID to the ID token, used for non-dpop applications
2 parents 60464a6 + 06767b4 commit 8ed8a4c

File tree

5 files changed

+179
-6
lines changed

5 files changed

+179
-6
lines changed

src/Config/KeysInterface.php

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

55
use Defuse\Crypto\Key as CryptoKey;
6-
use Lcobucci\JWT\Signer\Key\InMemory as Key;
6+
use Lcobucci\JWT\Signer\Key as Key;
77
use League\OAuth2\Server\CryptKey;
88

99
interface KeysInterface

src/TokenGenerator.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Pdsinterop\Solid\Auth\Exception\InvalidTokenException;
66
use Pdsinterop\Solid\Auth\Utils\DPop;
7+
use Pdsinterop\Solid\Auth\Utils\Jwks;
78
use Pdsinterop\Solid\Auth\Enum\OpenId\OpenIdConnectMetadata as OidcMeta;
89
use Laminas\Diactoros\Response\JsonResponse;
910
use League\OAuth2\Server\CryptTrait;
@@ -88,6 +89,10 @@ public function generateIdToken($accessToken, $clientId, $subject, $nonce, $priv
8889
$token = $token->withClaim("cnf", [
8990
"jkt" => $jkt,
9091
]);
92+
} else {
93+
// legacy mode
94+
$jwks = $this->getJwks();
95+
$token = $token->withHeader('kid', $jwks['keys'][0]['kid']);
9196
}
9297

9398
return $token->getToken($jwtConfig->signer(), $jwtConfig->signingKey())->toString();
@@ -201,4 +206,10 @@ private function makeJwkThumbprint($dpop): string
201206

202207
return $this->dpopUtil->makeJwkThumbprint($jwk);
203208
}
209+
210+
private function getJwks() {
211+
$key = $this->config->getKeys()->getPublicKey();
212+
$jwks = new Jwks($key);
213+
return json_decode((string) $jwks, true);
214+
}
204215
}

src/Utils/Bearer.php

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Pdsinterop\Solid\Auth\Utils;
4+
5+
use DateInterval;
6+
use Exception;
7+
use Jose\Component\Core\JWK;
8+
use Jose\Component\Core\Util\ECKey;
9+
use Jose\Component\Core\Util\RSAKey;
10+
use Lcobucci\Clock\SystemClock;
11+
use Lcobucci\JWT\Configuration;
12+
use Lcobucci\JWT\Signer\Ecdsa\Sha256;
13+
use Lcobucci\JWT\Signer\Key\InMemory;
14+
use Lcobucci\JWT\Token\InvalidTokenStructure;
15+
use Lcobucci\JWT\Validation\Constraint\LooseValidAt;
16+
use Lcobucci\JWT\Validation\Constraint\SignedWith;
17+
use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
18+
use Pdsinterop\Solid\Auth\Exception\AuthorizationHeaderException;
19+
use Pdsinterop\Solid\Auth\Exception\InvalidTokenException;
20+
use Psr\Http\Message\ServerRequestInterface;
21+
22+
/**
23+
* This class contains code to fetch the WebId from a request
24+
* that is make in legacy mode (bearer token with pop)
25+
*
26+
* @ TODO: Make sure this code complies with the spec and validate the tokens properly;
27+
* https://datatracker.ietf.org/doc/html/rfc7800
28+
*/
29+
class Bearer {
30+
31+
private JtiValidator $jtiValidator;
32+
33+
public function __construct(JtiValidator $jtiValidator)
34+
{
35+
$this->jtiValidator = $jtiValidator;
36+
}
37+
38+
/**
39+
* This method fetches the WebId from a request and verifies
40+
* that the request has a valid pop token that matches
41+
* the access token.
42+
*
43+
* @param ServerRequestInterface $request Server Request
44+
*
45+
* @return string the WebId, or "public" if no WebId is found
46+
*
47+
* @throws Exception "Invalid token" when the pop token is invalid
48+
*/
49+
public function getWebId($request) {
50+
$serverParams = $request->getServerParams();
51+
52+
if (empty($serverParams['HTTP_AUTHORIZATION'])) {
53+
$webId = "public";
54+
} else {
55+
$this->validateRequestHeaders($serverParams);
56+
57+
[, $jwt] = explode(" ", $serverParams['HTTP_AUTHORIZATION'], 2);
58+
59+
try {
60+
$this->validateJwt($jwt, $request);
61+
} catch (RequiredConstraintsViolated $e) {
62+
throw new InvalidTokenException($e->getMessage(), 0, $e);
63+
}
64+
$idToken = $this->getIdTokenFromJwt($jwt);
65+
66+
try {
67+
$this->validateIdToken($idToken, $request);
68+
} catch (RequiredConstraintsViolated $e) {
69+
throw new InvalidTokenException($e->getMessage(), 0, $e);
70+
}
71+
$webId = $this->getSubjectFromIdToken($idToken);
72+
}
73+
74+
return $webId;
75+
}
76+
77+
/**
78+
* @param string $jwt JWT access token, raw
79+
* @param ServerRequestInterface $request Server Request
80+
* @return bool
81+
*
82+
* FIXME: Add more validations to the token;
83+
*/
84+
public function validateJwt($jwt, $request) {
85+
$jwtConfig = Configuration::forUnsecuredSigner();
86+
$jwtConfig->parser()->parse($jwt);
87+
return true;
88+
}
89+
90+
/**
91+
* validates that the provided OIDC ID Token
92+
* @param string $token The OIDS ID Token (raw)
93+
* @param ServerRequestInterface $request Server Request
94+
* @return bool True if the id token is valid
95+
* @throws InvalidTokenException when the tokens is not valid
96+
*
97+
* FIXME: Add more validations to the token;
98+
*/
99+
public function validateIdToken($token, $request) {
100+
$jwtConfig = Configuration::forUnsecuredSigner();
101+
$jwtConfig->parser()->parse($token);
102+
return true;
103+
}
104+
105+
private function getIdTokenFromJwt($jwt) {
106+
$jwtConfig = Configuration::forUnsecuredSigner();
107+
try {
108+
$jwt = $jwtConfig->parser()->parse($jwt);
109+
} catch(Exception $e) {
110+
throw new InvalidTokenException("Invalid JWT token", 409, $e);
111+
}
112+
113+
$idToken = $jwt->claims()->get("id_token");
114+
if ($idToken === null) {
115+
throw new InvalidTokenException('Missing "id_token"');
116+
}
117+
return $idToken;
118+
}
119+
120+
private function getSubjectFromIdToken($idToken) {
121+
$jwtConfig = Configuration::forUnsecuredSigner();
122+
try {
123+
$jwt = $jwtConfig->parser()->parse($idToken);
124+
} catch(Exception $e) {
125+
throw new InvalidTokenException("Invalid ID token", 409, $e);
126+
}
127+
128+
$sub = $jwt->claims()->get("sub");
129+
if ($sub === null) {
130+
throw new InvalidTokenException('Missing "sub"');
131+
}
132+
return $sub;
133+
}
134+
135+
private function validateRequestHeaders($serverParams) {
136+
if (str_contains($serverParams['HTTP_AUTHORIZATION'], ' ') === false) {
137+
throw new AuthorizationHeaderException("Authorization Header does not contain parameters");
138+
}
139+
140+
if (str_starts_with(strtolower($serverParams['HTTP_AUTHORIZATION']), 'bearer') === false) {
141+
throw new AuthorizationHeaderException('Only "bearer" authorization scheme is supported');
142+
}
143+
}
144+
}

src/Utils/Jwks.php

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
namespace Pdsinterop\Solid\Auth\Utils;
44

55
use JsonSerializable;
6-
use Lcobucci\JWT\Signer\Key\InMemory;
6+
use Lcobucci\JWT\Signer\Key as Key;
77
use Pdsinterop\Solid\Auth\Enum\Jwk\Parameter as JwkParameter;
88
use Pdsinterop\Solid\Auth\Enum\Rsa\Parameter as RsaParameter;
99

1010
class Jwks implements JsonSerializable
1111
{
1212
////////////////////////////// CLASS PROPERTIES \\\\\\\\\\\\\\\\\\\\\\\\\\\\
1313

14-
/** @var InMemory */
14+
/** @var Key */
1515
private $publicKey;
1616

1717
//////////////////////////////// PUBLIC API \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
1818

19-
final public function __construct(InMemory $publicKey)
19+
final public function __construct(Key $publicKey)
2020
{
2121
$this->publicKey = $publicKey;
2222
}
@@ -64,9 +64,8 @@ private function create() : array
6464

6565
$publicKeys = [$this->publicKey];
6666

67-
array_walk($publicKeys, function (InMemory $publicKey) use (&$jwks) {
67+
array_walk($publicKeys, function (Key $publicKey) use (&$jwks) {
6868
$certificate = $publicKey->contents();
69-
7069
$key = openssl_pkey_get_public($certificate);
7170
$keyInfo = openssl_pkey_get_details($key);
7271

tests/unit/TokenGeneratorTest.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ final public function testIdTokenGenerationWithoutPrivateKey(): void
281281
* @testdox Token Generator SHOULD generate a token without Confirmation JWT Thumbprint (CNF JKT) WHEN asked to generate a IdToken without dpopKey
282282
*
283283
* @covers ::generateIdToken
284+
*
285+
* @uses \Pdsinterop\Solid\Auth\Utils\Jwks
284286
*/
285287
final public function testIdTokenGenerationWithoutDpopKey(): void
286288
{
@@ -305,6 +307,22 @@ final public function testIdTokenGenerationWithoutDpopKey(): void
305307
->willReturn('mock issuer')
306308
;
307309

310+
$publicKey = file_get_contents(__DIR__.'/../fixtures/keys/public.key');
311+
312+
$mockPublicKey = $this->getMockBuilder(\Lcobucci\JWT\Signer\Key::class)
313+
->getMock()
314+
;
315+
316+
$mockPublicKey->expects($this->once())
317+
->method('contents')
318+
->willReturn($publicKey)
319+
;
320+
321+
$this->mockKeys->expects($this->once())
322+
->method('getPublicKey')
323+
->willReturn($mockPublicKey)
324+
;
325+
308326
$privateKey = file_get_contents(__DIR__.'/../fixtures/keys/private.key');
309327

310328
$now = new \DateTimeImmutable('1234-01-01 12:34:56.789');
@@ -323,6 +341,7 @@ final public function testIdTokenGenerationWithoutDpopKey(): void
323341
[
324342
'typ' => 'JWT',
325343
'alg' => 'RS256',
344+
'kid' => '0c3932ca20f3a00ad2eb72035f6cc9cb'
326345
],
327346
[
328347
'at_hash' => '1EZBnvsFWlK8ESkgHQsrIQ',

0 commit comments

Comments
 (0)