diff --git a/src/TokenGenerator.php b/src/TokenGenerator.php index e11570a..ee48cff 100644 --- a/src/TokenGenerator.php +++ b/src/TokenGenerator.php @@ -52,37 +52,45 @@ public function generateRegistrationAccessToken($clientId, $privateKey) { return $token->toString(); } - public function generateIdToken($accessToken, $clientId, $subject, $nonce, $privateKey, $dpop, $now=null) { + /** + * Please note that the DPOP _is not_ required when requesting a token to + * authorize a client but the DPOP _is_ required when requesting an access + * token. + */ + public function generateIdToken($accessToken, $clientId, $subject, $nonce, $privateKey, $dpop=null, $now=null) { $issuer = $this->config->getServer()->get(OidcMeta::ISSUER); $tokenHash = $this->generateTokenHash($accessToken); - // Create JWT - $jwtConfig = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($privateKey)); - $now = $now ?? new DateTimeImmutable(); - $useAfter = $now->sub(new \DateInterval('PT1S')); - - $expire = $now->add($this->validFor); - - $jkt = $this->makeJwkThumbprint($dpop); - - $token = $jwtConfig->builder() - ->issuedBy($issuer) - ->permittedFor($clientId) - ->issuedAt($now) - ->canOnlyBeUsedAfter($useAfter) - ->expiresAt($expire) - ->withClaim("azp", $clientId) - ->relatedTo($subject) - ->identifiedBy($this->generateJti()) - ->withClaim("nonce", $nonce) - ->withClaim("at_hash", $tokenHash) //FIXME: at_hash should only be added if the response_type is a token - ->withClaim("c_hash", $tokenHash) // FIXME: c_hash should only be added if the response_type is a code - ->withClaim("cnf", array( - "jkt" => $jkt, - )) - ->getToken($jwtConfig->signer(), $jwtConfig->signingKey()); - return $token->toString(); + // Create JWT + $jwtConfig = Configuration::forSymmetricSigner(new Sha256(), InMemory::plainText($privateKey)); + $now = $now ?? new DateTimeImmutable(); + $useAfter = $now->sub(new \DateInterval('PT1S')); + + $expire = $now->add($this->validFor); + + $token = $jwtConfig->builder() + ->issuedBy($issuer) + ->permittedFor($clientId) + ->issuedAt($now) + ->canOnlyBeUsedAfter($useAfter) + ->expiresAt($expire) + ->withClaim("azp", $clientId) + ->relatedTo($subject) + ->identifiedBy($this->generateJti()) + ->withClaim("nonce", $nonce) + ->withClaim("at_hash", $tokenHash) //FIXME: at_hash should only be added if the response_type is a token + ->withClaim("c_hash", $tokenHash) // FIXME: c_hash should only be added if the response_type is a code + ; + + if ($dpop !== null) { + $jkt = $this->makeJwkThumbprint($dpop); + $token = $token->withClaim("cnf", [ + "jkt" => $jkt, + ]); + } + + return $token->getToken($jwtConfig->signer(), $jwtConfig->signingKey())->toString(); } public function respondToRegistration($registration, $privateKey) { diff --git a/tests/unit/TokenGeneratorTest.php b/tests/unit/TokenGeneratorTest.php index 3afaa26..17a01c7 100644 --- a/tests/unit/TokenGeneratorTest.php +++ b/tests/unit/TokenGeneratorTest.php @@ -278,27 +278,70 @@ final public function testIdTokenGenerationWithoutPrivateKey(): void } /** - * @testdox Token Generator SHOULD complain WHEN asked to generate a IdToken without dpopKey + * @testdox Token Generator SHOULD generate a token without Confirmation JWT Thumbprint (CNF JKT) WHEN asked to generate a IdToken without dpopKey * * @covers ::generateIdToken */ final public function testIdTokenGenerationWithoutDpopKey(): void { - $tokenGenerator = $this->createTokenGenerator(); + $validFor = new \DateInterval('PT1S'); - $this->expectArgumentCountError(6); + $tokenGenerator = $this->createTokenGenerator($validFor); - $tokenGenerator->generateIdToken( + + $mockServer = $this->getMockBuilder(ServerInterface::class) + ->disableOriginalConstructor() + ->getMock() + ; + + $this->mockConfig->expects($this->once()) + ->method('getServer') + ->willReturn($mockServer) + ; + + $mockServer->expects($this->once()) + ->method('get') + ->with(OidcMeta::ISSUER) + ->willReturn('mock issuer') + ; + + $privateKey = file_get_contents(__DIR__.'/../fixtures/keys/private.key'); + + $now = new \DateTimeImmutable('1234-01-01 12:34:56.789'); + + $token = $tokenGenerator->generateIdToken( 'mock access token', 'mock clientId', 'mock subject', 'mock nonce', - 'mock private key' + $privateKey, + null, + $now, ); + + $this->assertJwtEquals([ + [ + 'typ' => 'JWT', + 'alg' => 'RS256', + ], + [ + 'at_hash' => '1EZBnvsFWlK8ESkgHQsrIQ', + 'aud' => 'mock clientId', + 'azp' => 'mock clientId', + 'c_hash' => '1EZBnvsFWlK8ESkgHQsrIQ', + 'exp' => -23225829903.789, + 'iat' => -23225829904.789, + 'iss' => 'mock issuer', + 'jti' => '4dc20036dbd8313ed055', + 'nbf' => -23225829905.789, + 'nonce' => 'mock nonce', + 'sub' => 'mock subject', + ], + ], $token); } /** - * @testdox Token Generator SHOULD return a IdToken WHEN asked to generate a IdToken with clientId and privateKey + * @testdox Token Generator SHOULD return a IdToken with a Confirmation JWT Thumbprint (CNF JKT) WHEN asked to generate a IdToken with clientId and privateKey and DPOP * * @covers ::generateIdToken *