Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 24 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@ Currently supported flows are:

![Main screen capture](docs/oidc.png)

### Note on OpenID Federation (OIDF) support

OpenID Federation support is in "draft" phase, as is the
[specification](https://openid.net/specs/openid-federation-1_0) itself. This means that you can expect braking changes
in future releases related to OIDF capabilities. You can enable / disable OIDF support at any time in module
configuration.

Currently, the following OIDF features are supported:
* automatic client registration using a Request Object (passing it by value)
* endpoint for issuing configuration entity statement (statement about itself)
* fetch endpoint for issuing statements about subordinates (registered clients)

OIDF support is implemented using the underlying [SimpleSAMLphp OpenID library](https://github.com/simplesamlphp/openid).

## Version compatibility

Minor versions of SimpleSAMLphp noted below means that the module has been tested with that version of SimpleSAMLphp
Expand Down Expand Up @@ -150,6 +164,16 @@ Once you deploy the module, in the SimpleSAMLphp administration area go to `OIDC
Protocol / Federation Settings page to see the available discovery URLs. These URLs can then be used to set up a
`.well-known` URLs (see below).

### Key rollover

The module supports defining additional (new) private / public key pair to be published on relevant JWKS endpoint
or contained in relevant JWKS property. In this way, you can "announce" new public key which can then be fetched
by RPs in order to prepare for the switch of the keys (until the switch of keys, all artifacts continue to be
signed with the "old" private key).

In this way, after RPs fetch new JWKS (JWKS with "old" and "new" key), you can do the switch of keys when you find
appropriate.

### Note when using Apache web server

If you are using Apache web server, you might encounter situations in which Apache strips of Authorization header
Expand All @@ -168,20 +192,6 @@ SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
```
Choose the one which works for you. If you don't set it, you'll get a warnings about this situation in your logs.

### Note on OpenID Federation (OIDF) support

OpenID Federation support is in "draft" phase, as is the
[specification](https://openid.net/specs/openid-federation-1_0) itself. This means that you can expect braking changes
in future releases related to OIDF capabilities. You can enable / disable OIDF support at any time in module
configuration.

Currently, the following OIDF features are supported:
* endpoint for issuing configuration entity statement (statement about itself)
* fetch endpoint for issuing statements about subordinates (registered clients)
* automatic client registration using a Request Object

OIDF support is implemented using the underlying [SimpleSAMLphp OpenID library](https://github.com/simplesamlphp/openid).

## Additional considerations
### Private scopes

Expand Down
5 changes: 4 additions & 1 deletion UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
client and user data. The cache layer stands in front of the database store, so it can improve performance, especially
in cases of sudden surge of users trying to authenticate. Implementation is based on Symfony Cache component, so any
compatible Symfony cache adapter can be used. Check the module config file for more information on how to set the
protocol cache.
protocol cache.
- Key rollover support - you can now define additional (new) private / public key pair which will be published on
relevant JWKS endpoint or contained in JWKS property. In this way, you can "announce" new public key which can then
be fetched by RPs, and do the switch between "old" and "new" key pair when you find appropriate.
- OpenID capabilities
- New federation endpoints:
- endpoint for issuing configuration entity statement (statement about itself)
Expand Down
33 changes: 27 additions & 6 deletions config/module_oidc.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,30 @@ $config = [
* is a case-sensitive URL using the https scheme that contains scheme, host, and optionally, port number and
* path components and no query or fragment components."
*/
//ModuleConfig::OPTION_ISSUER => 'https://op.example.org',
// ModuleConfig::OPTION_ISSUER => 'https://op.example.org',

/**
* PKI (public / private key) settings related to OIDC protocol. These keys will be used, for example, to
* sign ID Token JWT.
*/
// (optional) The private key passphrase.
//ModuleConfig::OPTION_PKI_PRIVATE_KEY_PASSPHRASE => 'secret',
// ModuleConfig::OPTION_PKI_PRIVATE_KEY_PASSPHRASE => 'secret',
// 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,

/**
* (optional) Key rollover settings related to OIDC protocol. If set, this new private / public key pair will only
* be published on JWKS endpoint as available, so Relying Parties can pick them up for future use. The signing
* of artifacts will still be done using the 'current' private key (settings above). After some time, when all
* RPs have fetched all public keys from JWKS endpoint, simply set these new keys as active values for above
* PKI options.
*/
// // (optional) The (new) private key passphrase.
// ModuleConfig::OPTION_PKI_NEW_PRIVATE_KEY_PASSPHRASE => 'new-secret',
// ModuleConfig::OPTION_PKI_NEW_PRIVATE_KEY_FILENAME => 'new_oidc_module.key',
// ModuleConfig::OPTION_PKI_NEW_CERTIFICATE_FILENAME => 'new_oidc_module.crt',

/**
* Token related options.
*/
Expand All @@ -51,8 +63,8 @@ $config = [
// Token signer, with given default.
// See Lcobucci\JWT\Signer algorithms in https://github.com/lcobucci/jwt/tree/master/src/Signer
ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Rsa\Sha256::class,
//ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
//ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class,
// ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Hmac\Sha256::class,
// ModuleConfig::OPTION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Ecdsa\Sha256::class,

/**
* Authentication related options.
Expand Down Expand Up @@ -347,7 +359,7 @@ $config = [
// Federation authority hints. An array of strings representing the Entity Identifiers of Intermediate Entities
// (or Trust Anchors). Required if this entity has a Superior entity above it.
ModuleConfig::OPTION_FEDERATION_AUTHORITY_HINTS => [
//'https://intermediate.example.org/',
// 'https://intermediate.example.org/',
],

// (optional) Federation Trust Mark tokens. An array of tokens (signed JWTs), each representing a Trust Mark
Expand Down Expand Up @@ -411,13 +423,22 @@ $config = [
* entity statements. Note that these keys SHOULD NOT be the same as the ones used in OIDC protocol itself.
*/
// The federation private key passphrase (optional).
//ModuleConfig::OPTION_PKI_FEDERATION_PRIVATE_KEY_PASSPHRASE => 'secret',
// 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,

/**
* (optional) Key rollover settings related to OpenID Federation. Check the OIDC protocol key rollover description
* on how this works.
*/
// The federation (new) private key passphrase (optional).
// ModuleConfig::OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_PASSPHRASE => 'new-secret',
// ModuleConfig::OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_FILENAME => 'new_oidc_module_federation.key',
// ModuleConfig::OPTION_PKI_FEDERATION_NEW_CERTIFICATE_FILENAME => 'new_oidc_module_federation.crt',

// Federation token signer, with given default.
ModuleConfig::OPTION_FEDERATION_TOKEN_SIGNER => \Lcobucci\JWT\Signer\Rsa\Sha256::class,

Expand Down
44 changes: 43 additions & 1 deletion src/ModuleConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ class ModuleConfig
final public const OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS =
'federation_participation_limit_by_trust_marks';

final public const OPTION_PKI_NEW_PRIVATE_KEY_PASSPHRASE = 'new_private_key_passphrase';
final public const OPTION_PKI_NEW_PRIVATE_KEY_FILENAME = 'new_privatekey';
final public const OPTION_PKI_NEW_CERTIFICATE_FILENAME = 'new_certificate';

final public const OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_PASSPHRASE = 'federation_new_private_key_passphrase';
final public const OPTION_PKI_FEDERATION_NEW_PRIVATE_KEY_FILENAME = 'federation_new_private_key_filename';
final public const OPTION_PKI_FEDERATION_NEW_CERTIFICATE_FILENAME = 'federation_new_certificate_filename';

protected static array $standardScopes = [
ScopesEnum::OpenId->value => [
self::KEY_DESCRIPTION => 'openid',
Expand Down Expand Up @@ -365,6 +373,22 @@ public function getProtocolCertPath(): string
return $this->sspBridge->utils()->config()->getCertPath($certName);
}

/**
* Get the path to the new public certificate to be used in OIDC protocol.
* @return ?string Null if not set, or file system path
* @throws \Exception
*/
public function getProtocolNewCertPath(): ?string
{
$certName = $this->config()->getOptionalString(self::OPTION_PKI_NEW_CERTIFICATE_FILENAME, null);

if (is_string($certName)) {
return $this->sspBridge->utils()->config()->getCertPath($certName);
}

return null;
}

/**
* Get supported Authentication Context Class References (ACRs).
*
Expand Down Expand Up @@ -522,7 +546,6 @@ public function getFederationPrivateKeyPassPhrase(): ?string

/**
* Return the path to the federation public certificate
* @return string The file system path or null if not set.
* @throws \Exception
*/
public function getFederationCertPath(): string
Expand All @@ -535,6 +558,25 @@ public function getFederationCertPath(): string
return $this->sspBridge->utils()->config()->getCertPath($certName);
}

/**
* Return the path to the new federation public certificate
* @return ?string The file system path or null if not set.
* @throws \Exception
*/
public function getFederationNewCertPath(): ?string
{
$certName = $this->config()->getOptionalString(
self::OPTION_PKI_FEDERATION_NEW_CERTIFICATE_FILENAME,
null,
);

if (is_string($certName)) {
return $this->sspBridge->utils()->config()->getCertPath($certName);
}

return null;
}

/**
* @throws \Exception
*/
Expand Down
104 changes: 75 additions & 29 deletions src/Services/JsonWebKeySetService.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,42 +27,20 @@
class JsonWebKeySetService
{
/** @var JWKSet JWKS for OIDC protocol. */
private readonly JWKSet $protocolJwkSet;
protected JWKSet $protocolJwkSet;
/** @var JWKSet|null JWKS for OpenID Federation. */
private ?JWKSet $federationJwkSet = null;
protected ?JWKSet $federationJwkSet = null;

/**
* @throws \SimpleSAML\Error\Exception
* @throws \Exception
*/
public function __construct(ModuleConfig $moduleConfig)
{
$publicKeyPath = $moduleConfig->getProtocolCertPath();
if (!file_exists($publicKeyPath)) {
throw new Error\Exception("OIDC protocol public key file does not exists: $publicKeyPath.");
}
public function __construct(
protected readonly ModuleConfig $moduleConfig,
) {
$this->prepareProtocolJwkSet();

$jwk = JWKFactory::createFromKeyFile($publicKeyPath, null, [
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($publicKeyPath),
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
ClaimsEnum::Alg->value => $moduleConfig->getProtocolSigner()->algorithmId(),
]);

$this->protocolJwkSet = new JWKSet([$jwk]);

if (
($federationPublicKeyPath = $moduleConfig->getFederationCertPath()) &&
file_exists($federationPublicKeyPath) &&
($federationSigner = $moduleConfig->getFederationSigner())
) {
$federationJwk = JWKFactory::createFromKeyFile($federationPublicKeyPath, null, [
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($federationPublicKeyPath),
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
ClaimsEnum::Alg->value => $federationSigner->algorithmId(),
]);

$this->federationJwkSet = new JWKSet([$federationJwk]);
}
$this->prepareFederationJwkSet();
}

/**
Expand All @@ -84,4 +62,72 @@ public function federationKeys(): array

return $this->federationJwkSet->all();
}

/**
* @throws \ReflectionException
* @throws \SimpleSAML\Error\Exception
*/
protected function prepareProtocolJwkSet(): void
{
$protocolPublicKeyPath = $this->moduleConfig->getProtocolCertPath();

if (!file_exists($protocolPublicKeyPath)) {
throw new Error\Exception("OIDC protocol public key file does not exists: $protocolPublicKeyPath.");
}

$jwk = JWKFactory::createFromKeyFile($protocolPublicKeyPath, null, [
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($protocolPublicKeyPath),
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
ClaimsEnum::Alg->value => $this->moduleConfig->getProtocolSigner()->algorithmId(),
]);

$keys = [$jwk];

if (
($protocolNewPublicKeyPath = $this->moduleConfig->getProtocolNewCertPath()) &&
file_exists($protocolNewPublicKeyPath)
) {
$newJwk = JWKFactory::createFromKeyFile($protocolNewPublicKeyPath, null, [
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($protocolNewPublicKeyPath),
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
ClaimsEnum::Alg->value => $this->moduleConfig->getProtocolSigner()->algorithmId(),
]);

$keys[] = $newJwk;
}

$this->protocolJwkSet = new JWKSet($keys);
}

protected function prepareFederationJwkSet(): void
{
$federationPublicKeyPath = $this->moduleConfig->getFederationCertPath();

if (!file_exists($federationPublicKeyPath)) {
return;
}

$federationJwk = JWKFactory::createFromKeyFile($federationPublicKeyPath, null, [
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($federationPublicKeyPath),
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
ClaimsEnum::Alg->value => $this->moduleConfig->getFederationSigner()->algorithmId(),
]);

$keys = [$federationJwk];

if (
($federationNewPublicKeyPath = $this->moduleConfig->getFederationNewCertPath()) &&
file_exists($federationNewPublicKeyPath)
) {
$federationNewJwk = JWKFactory::createFromKeyFile($federationNewPublicKeyPath, null, [
ClaimsEnum::Kid->value => FingerprintGenerator::forFile($federationNewPublicKeyPath),
ClaimsEnum::Use->value => PublicKeyUseEnum::Signature->value,
ClaimsEnum::Alg->value => $this->moduleConfig->getFederationSigner()->algorithmId(),
]);

$keys[] = $federationNewJwk;
}

$this->federationJwkSet = new JWKSet($keys);
}
}
Loading