Skip to content

Commit

Permalink
WIP Refactor JWT Builder
Browse files Browse the repository at this point in the history
  • Loading branch information
cicnavi committed May 21, 2024
1 parent 84610fc commit 9523140
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 12 deletions.
18 changes: 16 additions & 2 deletions config-templates/module_oidc.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@
*/
$config = [
/**
* PKI (public / private key) related options.
* PKI (public / private key) related options related to OIDC protocol. These keys will be used, for example, to
* sign ID Token JWT.
*/
// The private key passphrase (optional).
//ModuleConfig::OPTION_PKI_PRIVATE_KEY_PASSPHRASE => 'secret',
// The certificate and private key filenames for ID token signature handling, with given defaults.
// The certificate and private key filenames, with given defaults.
ModuleConfig::OPTION_PKI_PRIVATE_KEY_FILENAME => ModuleConfig::DEFAULT_PKI_PRIVATE_KEY_FILENAME,
ModuleConfig::OPTION_PKI_CERTIFICATE_FILENAME => ModuleConfig::DEFAULT_PKI_CERTIFICATE_FILENAME,

Expand All @@ -44,6 +45,19 @@
//ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
//ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class,

/**
* (optional) PKI options related to OpenID Federation. These keys will be used, for example, to sign federation
* entity statements. Note that these keys SHOULD NOT be the same as the ones used in OIDC protocol itself.
* If these are not set, OpenID federation capabilities will be disabled.
*/
// The federation private key passphrase (optional).
//ModuleConfig::OPTION_PKI_FEDERATION_PRIVATE_KEY_PASSPHRASE => 'secret',
// The federation certificate and private key filenames, with given defaults.
//ModuleConfig::OPTION_PKI_FEDERATION_PRIVATE_KEY_FILENAME =>
// ModuleConfig::DEFAULT_PKI_FEDERATION_PRIVATE_KEY_FILENAME,
//ModuleConfig::OPTION_PKI_FEDERATION_CERTIFICATE_FILENAME =>
// ModuleConfig::DEFAULT_PKI_FEDERATION_CERTIFICATE_FILENAME,

/**
* Authentication related options.
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Entities/AccessTokenEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ protected function convertToJWT(): Token
{
$jwtBuilderService = new JsonWebTokenBuilderService();
/** @psalm-suppress ArgumentTypeCoercion */
$jwtBuilder = $jwtBuilderService->getDefaultJwtTokenBuilder()
$jwtBuilder = $jwtBuilderService->getProtocolJwtBuilder()
->permittedFor($this->getClient()->getIdentifier())
->identifiedBy((string)$this->getIdentifier())
->issuedAt(new DateTimeImmutable())
Expand Down
41 changes: 40 additions & 1 deletion src/ModuleConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ class ModuleConfig
final public const OPTION_CRON_TAG = 'cron_tag';
final public const OPTION_ADMIN_UI_PERMISSIONS = 'permissions';
final public const OPTION_ADMIN_UI_PAGINATION_ITEMS_PER_PAGE = 'items_per_page';
final public const OPTION_FEDERATION_TOKEN_SIGNER = 'federation_token_signer';
final public const OPTION_PKI_FEDERATION_PRIVATE_KEY_PASSPHRASE = 'federation_private_key_passphrase';
final public const OPTION_PKI_FEDERATION_PRIVATE_KEY_FILENAME = 'federation_private_key_filename';
final public const DEFAULT_PKI_FEDERATION_PRIVATE_KEY_FILENAME = 'oidc_module_federation.key';
final public const OPTION_PKI_FEDERATION_CERTIFICATE_FILENAME = 'federation_certificate_filename';
final public const DEFAULT_PKI_FEDERATION_CERTIFICATE_FILENAME = 'oidc_module_federation.crt';

protected static array $standardClaims = [
// TODO mivanci Move registered scopes to enum?
Expand Down Expand Up @@ -237,7 +243,16 @@ public function getSigner(): Signer
Sha256::class
);

$class = new ReflectionClass($signerClassname);
return $this->instantiateSigner($signerClassname);
}

/**
* @throws ReflectionException
* @param class-string $className
*/
protected function instantiateSigner(string $className): Signer
{
$class = new ReflectionClass($className);
$signer = $class->newInstance();

if (!$signer instanceof Signer) {
Expand Down Expand Up @@ -348,4 +363,28 @@ public function getUserIdentifierAttribute(): string
{
return $this->config()->getString(ModuleConfig::OPTION_AUTH_USER_IDENTIFIER_ATTRIBUTE);
}

public function getFederationSigner(): ?Signer
{
/** @psalm-var ?class-string $signerClassname */
$signerClassname = $this->config()->getOptionalString(self::OPTION_FEDERATION_TOKEN_SIGNER, null);

return is_null($signerClassname) ? null : $this->instantiateSigner($signerClassname);
}

public function getFederationPrivateKeyPath(): ?string
{
$keyName = $this->config()->getOptionalString(
self::OPTION_PKI_FEDERATION_PRIVATE_KEY_FILENAME,
null
);

// TODO mivanci move to bridge classes to SSP utils
return is_null($keyName) ? null : (new Config())->getCertPath($keyName);
}

public function getFederationPrivateKeyPassPhrase(): ?string
{
return $this->config()->getOptionalString(self::OPTION_PKI_FEDERATION_PRIVATE_KEY_PASSPHRASE, null);
}
}
2 changes: 1 addition & 1 deletion src/Services/IdTokenBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ protected function getBuilder(
): Builder {
/** @psalm-suppress ArgumentTypeCoercion */
return $this->jsonWebTokenBuilderService
->getDefaultJwtTokenBuilder()
->getProtocolJwtBuilder()
->permittedFor($accessToken->getClient()->getIdentifier())
->identifiedBy($accessToken->getIdentifier())
->canOnlyBeUsedAfter(new DateTimeImmutable('now'))
Expand Down
49 changes: 46 additions & 3 deletions src/Services/JsonWebTokenBuilderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@
use League\OAuth2\Server\Exception\OAuthServerException;
use ReflectionException;
use SimpleSAML\Module\oidc\ModuleConfig;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
use SimpleSAML\Module\oidc\Utils\FingerprintGenerator;
use SimpleSAML\Module\oidc\Utils\UniqueIdentifierGenerator;

class JsonWebTokenBuilderService
{
/**
* @var Configuration Token configuration related to OIDC protocol.
*/
protected Configuration $jwtConfig;

/**
* @var ?Configuration Token configuration related to OpenID Federation.
*/
protected ?Configuration $federationJwtConfig = null;

/**
* @throws ReflectionException
* @throws Exception
Expand All @@ -39,16 +48,50 @@ public function __construct(
),
InMemory::plainText('empty', 'empty')
);

if (
($federationSigner = $this->moduleConfig->getFederationSigner()) &&
($federationPrivateKeyPath = $this->moduleConfig->getFederationPrivateKeyPath())
) {
$this->federationJwtConfig = Configuration::forAsymmetricSigner(
$federationSigner,
InMemory::file(
$federationPrivateKeyPath,
$this->moduleConfig->getFederationPrivateKeyPassPhrase() ?? ''
),
InMemory::plainText('empty', 'empty')
);
}
}

/**
* @throws OidcServerException
*/
public function getProtocolJwtBuilder(): Builder
{
return $this->getDefaultJwtBuilder($this->jwtConfig);
}

/**
* @throws OidcServerException
*/
public function getFederationJwtBuilder(): Builder
{
if (is_null($this->federationJwtConfig)) {
throw OidcServerException::serverError('Federation JWT PKI configuration is not set.');
}

return $this->getDefaultJwtBuilder($this->federationJwtConfig);
}

/**
* @throws OAuthServerException
* @throws OidcServerException
*/
public function getDefaultJwtTokenBuilder(): Builder
public function getDefaultJwtBuilder(Configuration $configuration): Builder
{
/** @psalm-suppress ArgumentTypeCoercion */
// Ignore microseconds when handling dates.
return $this->jwtConfig->builder(ChainedFormatter::withUnixTimestampDates())
return $configuration->builder(ChainedFormatter::withUnixTimestampDates())
->issuedBy($this->moduleConfig->getSimpleSAMLSelfURLHost())
->issuedAt(new DateTimeImmutable('now'))
->identifiedBy(UniqueIdentifierGenerator::hitMe());
Expand Down
2 changes: 1 addition & 1 deletion src/Services/LogoutTokenBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function __construct(
public function forRelyingPartyAssociation(RelyingPartyAssociationInterface $relyingPartyAssociation): string
{
$logoutTokenBuilder = $this->jsonWebTokenBuilderService
->getDefaultJwtTokenBuilder()
->getProtocolJwtBuilder()
->withHeader('typ', 'logout+jwt')
->permittedFor($relyingPartyAssociation->getClientId())
->relatedTo($relyingPartyAssociation->getUserId())
Expand Down
2 changes: 1 addition & 1 deletion src/Utils/UniqueIdentifierGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class UniqueIdentifierGenerator
/**
* Generate a new unique identifier.
*
* @throws OAuthServerException
* @throws OidcServerException
*/
public static function hitMe(int $length = 40): string
{
Expand Down
4 changes: 2 additions & 2 deletions tests/src/Services/JsonWebTokenBuilderServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public function testCanCreateBuilderInstance(): void

$this->assertInstanceOf(
Builder::class,
$builderService->getDefaultJwtTokenBuilder()
$builderService->getProtocolJwtBuilder()
);
}

Expand All @@ -76,7 +76,7 @@ public function testCanCreateBuilderInstance(): void
public function testCanGenerateSignedJwtToken(): void
{
$builderService = new JsonWebTokenBuilderService($this->moduleConfigStub);
$tokenBuilder = $builderService->getDefaultJwtTokenBuilder();
$tokenBuilder = $builderService->getProtocolJwtBuilder();

$unencryptedToken = $builderService->getSignedJwtTokenFromBuilder($tokenBuilder);

Expand Down

0 comments on commit 9523140

Please sign in to comment.