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
41 changes: 31 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Currently supported flows are:

[![Build Status](https://github.com/simplesamlphp/simplesamlphp-module-oidc/actions/workflows/test.yaml/badge.svg)](https://github.com/simplesamlphp/simplesamlphp-module-oidc/actions/workflows/test.yaml)
[![Coverage Status](https://codecov.io/gh/simplesamlphp/simplesamlphp-module-oidc/branch/master/graph/badge.svg)](https://app.codecov.io/gh/simplesamlphp/simplesamlphp-module-oidc)
[![SimpleSAMLphp](https://img.shields.io/badge/simplesamlphp-2.1-brightgreen)](https://simplesamlphp.org/)
[![SimpleSAMLphp](https://img.shields.io/badge/simplesamlphp-2.3-brightgreen)](https://simplesamlphp.org/)

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

Expand Down Expand Up @@ -112,17 +112,27 @@ Once the module is enabled, the database migrations must be run.
### Run database migrations

The module comes with some default SQL migrations which set up needed tables in the configured database. To run them,
go to `OIDC` > `Database Migrations`, and press the available button.
in the SimpleSAMLphp administration area go to `OIDC` > `Database Migrations`, and press the available button.

Alternatively, in case of automatic / scripted deployments, you can run the 'install.php' script from the command line:

php modules/oidc/bin/install.php

### Protocol Artifacts Caching

The configured database serves as the primary storage for protocol artifacts, such as access tokens, authorization
codes, refresh tokens, clients, and user data. In production environments, it is recommended to also set up caching
for these artifacts. The cache layer operates in front of the database, improving performance, particularly during
sudden surges of users attempting to authenticate. The implementation leverages the Symfony Cache component, allowing
the use of any compatible Symfony cache adapter. For more details on configuring the protocol cache, refer to the
module configuration file.

### Relying Party (RP) Administration

The module lets you manage (create, read, update and delete) approved RPs from the module user interface itself.

Once the database schema has been created, you can go to `OIDC` > `Client Registry`.
Once the database schema has been created, in the SimpleSAMLphp administration area go to `OIDC` >
`Client Registry`.

Note that clients can be marked as confidential or public. If the client is not marked as confidential (it is public),
and is using Authorization Code flow, it will have to provide PKCE parameters during the flow.
Expand All @@ -136,12 +146,9 @@ to be enabled and configured.

### Endpoint locations

Once you deployed the module, you will need the exact endpoint urls the module provides to configure the relying parties.
You can visit the discovery endpoint to learn this information:

`<basepath>/module.php/oidc/.well-known/openid-configuration`

This endpoint can be used to set up a `.well-known` URL (see below).
Once you deploy the module, in the SimpleSAMLphp administration area go to `OIDC` and then select the
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).

### Note when using Apache web server

Expand All @@ -161,6 +168,20 @@ 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 Expand Up @@ -343,7 +364,7 @@ You may view the OIDC configuration endpoint at `https://localhost/.well-known/o
To test local changes against another DB, such as Postgres, we need to:

* Create a docker network layer
* Run a DB container ( and create a DB if one doesn't exist)
* Run a DB container (and create a DB if one doesn't exist)
* Run SSP and use the DB container

```
Expand Down
8 changes: 6 additions & 2 deletions UPGRADE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@
# Version 5 to 6

## New features

- Caching support for OIDC protocol artifacts like Access Tokens, Authorization Codes, Refresh Tokens, but also
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.
- OpenID capabilities
- New federation endpoints:
- endpoint for issuing configuration entity statement (statement about itself)
Expand Down Expand Up @@ -40,7 +44,7 @@ https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication

- (optional) Issuer - you can now override the issuer (OP identifier). If not set, it falls back to current scheme, host
and optionally a port (as in all previous module versions).
- protocol caching adapter and its arguments
- (optional) Protocol caching adapter and its arguments
- (optional) OpenID Federation related options (needed if federation capabilities are to be used):
- enabled or disabled federation capabilities
- valid trust anchors
Expand Down
58 changes: 32 additions & 26 deletions config-templates/module_oidc.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,42 +258,48 @@
// also give proper adapter arguments for its instantiation below.
// @see https://symfony.com/doc/current/components/cache.html#available-cache-adapters
ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER => null,
//ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER => \Symfony\Component\Cache\Adapter\FilesystemAdapter::class,
//ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER => \Symfony\Component\Cache\Adapter\MemcachedAdapter::class,
// ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER => \Symfony\Component\Cache\Adapter\FilesystemAdapter::class,
// ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER => \Symfony\Component\Cache\Adapter\MemcachedAdapter::class,

// Federation cache adapter arguments used for adapter instantiation. Refer to documentation for particular
// Protocol cache adapter arguments used for adapter instantiation. Refer to documentation for particular
// adapter on which arguments are needed to create its instance, in the order of constructor arguments.
// See examples below.
ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS => [
// Adapter arguments here...
],
// Example for FileSystemAdapter:
//ModuleConfig::OPTION_FEDERATION_CACHE_ADAPTER_ARGUMENTS => [
// 'openidFederation', // Namespace, subdirectory of main cache directory
// 60 * 60 * 6, // Default lifetime in seconds (used when particular cache item doesn't define its own lifetime)
// '/path/to/main/cache/directory' // Must be writable. Can be set to null to use system temporary directory.
//],
// Example for MemcachedAdapter:
//ModuleConfig::OPTION_FEDERATION_CACHE_ADAPTER_ARGUMENTS => [
// // First argument is a connection instance, so we can use the helper method to create it. In this example a
// // single server is used. Refer to documentation on how to use multiple servers, and / or to provide other
// // options.
// \Symfony\Component\Cache\Adapter\MemcachedAdapter::createConnection(
// 'memcached://localhost'
// // the DSN can include config options (pass them as a query string):
// // 'memcached://localhost:11222?retry_timeout=10'
// // 'memcached://localhost:11222?socket_recv_size=1&socket_send_size=2'
// ),
// 'openidFederation', // Namespace, key prefix.
// 60 * 60 * 6, // Default lifetime in seconds (used when particular cache item doesn't define its own lifetime)
//],
// ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS => [
// 'openidFederation', // Namespace, subdirectory of main cache directory
// 60 * 60 * 6, // Default lifetime in seconds (used when particular cache item doesn't define its own lifetime)
// '/path/to/main/cache/directory' // Must be writable. Can be set to null to use system temporary directory.
// ],
// Example for MemcachedAdapter:
// ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS => [
// // First argument is a connection instance, so we can use the helper method to create it. In this example a
// // single server is used. Refer to documentation on how to use multiple servers, and / or to provide other
// // options.
// \Symfony\Component\Cache\Adapter\MemcachedAdapter::createConnection(
// 'memcached://localhost'
// // the DSN can include config options (pass them as a query string):
// // 'memcached://localhost:11222?retry_timeout=10'
// // 'memcached://localhost:11222?socket_recv_size=1&socket_send_size=2'
// ),
// 'openidProtocol', // Namespace, key prefix.
// 60 * 60 * 6, // Default lifetime in seconds (used when particular cache item doesn't define its own lifetime)
// ],

/**
* Protocol cache duration for particular entities. This is only relevant if protocol cache adapter is set up.
* For duration format info, check https://www.php.net/manual/en/dateinterval.construct.php.
*/
// Cache duration for user entities (authenticated users data). If not set, cache duration will be the same as
// session duration. This is used to avoid fetching user data from database on every authentication event.
// This is only relevant if protocol cache adapter is set up. For duration format info, check
// https://www.php.net/manual/en/dateinterval.construct.php.
// session duration.
// ModuleConfig::OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION => 'PT1H', // 1 hour
ModuleConfig::OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION => null, // fallback to session duration
ModuleConfig::OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION => null, // Fallback to session duration
// Cache duration for client entities, with given default.
ModuleConfig::OPTION_PROTOCOL_CLIENT_ENTITY_CACHE_DURATION => 'PT10M', // 10 minutes
// Cache duration for Authorization Code, Access Token, and Refresh Token will fall back to their TTL.


/**
* Cron related options.
Expand Down
3 changes: 1 addition & 2 deletions src/Entities/AccessTokenEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
use PDO;
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\EntityStringRepresentationInterface;
use SimpleSAML\Module\oidc\Entities\Traits\AssociateWithAuthCodeTrait;
Expand Down Expand Up @@ -112,7 +111,7 @@ public function getState(): array
'expires_at' => $this->getExpiryDateTime()->format('Y-m-d H:i:s'),
'user_id' => $this->getUserIdentifier(),
'client_id' => $this->getClient()->getIdentifier(),
'is_revoked' => [$this->isRevoked(), PDO::PARAM_BOOL],
'is_revoked' => $this->isRevoked(),
'auth_code_id' => $this->getAuthCodeId(),
'requested_claims' => json_encode($this->requestedClaims, JSON_THROW_ON_ERROR),
];
Expand Down
3 changes: 1 addition & 2 deletions src/Entities/AuthCodeEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use League\OAuth2\Server\Entities\ClientEntityInterface as OAuth2ClientEntityInterface;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
use PDO;
use SimpleSAML\Module\oidc\Entities\Interfaces\AuthCodeEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\MementoInterface;
use SimpleSAML\Module\oidc\Entities\Traits\OidcAuthCodeTrait;
Expand Down Expand Up @@ -66,7 +65,7 @@ public function getState(): array
'expires_at' => $this->getExpiryDateTime()->format('Y-m-d H:i:s'),
'user_id' => $this->getUserIdentifier(),
'client_id' => $this->client->getIdentifier(),
'is_revoked' => [$this->isRevoked(), PDO::PARAM_BOOL],
'is_revoked' => $this->isRevoked(),
'redirect_uri' => $this->getRedirectUri(),
'nonce' => $this->getNonce(),
];
Expand Down
7 changes: 3 additions & 4 deletions src/Entities/ClientEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use DateTimeImmutable;
use League\OAuth2\Server\Entities\Traits\ClientTrait;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use PDO;
use SimpleSAML\Module\oidc\Codebooks\RegistrationTypeEnum;
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
use SimpleSAML\OpenID\Codebooks\ClientRegistrationTypesEnum;
Expand Down Expand Up @@ -167,8 +166,8 @@ public function getState(): array
self::KEY_AUTH_SOURCE => $this->getAuthSourceId(),
self::KEY_REDIRECT_URI => json_encode($this->getRedirectUri(), JSON_THROW_ON_ERROR),
self::KEY_SCOPES => json_encode($this->getScopes(), JSON_THROW_ON_ERROR),
self::KEY_IS_ENABLED => [$this->isEnabled(), PDO::PARAM_BOOL],
self::KEY_IS_CONFIDENTIAL => [$this->isConfidential(), PDO::PARAM_BOOL],
self::KEY_IS_ENABLED => $this->isEnabled(),
self::KEY_IS_CONFIDENTIAL => $this->isConfidential(),
self::KEY_OWNER => $this->getOwner(),
self::KEY_POST_LOGOUT_REDIRECT_URI => json_encode($this->getPostLogoutRedirectUri(), JSON_THROW_ON_ERROR),
self::KEY_BACKCHANNEL_LOGOUT_URI => $this->getBackChannelLogoutUri(),
Expand All @@ -188,7 +187,7 @@ public function getState(): array
self::KEY_UPDATED_AT => $this->getUpdatedAt()?->format('Y-m-d H:i:s'),
self::KEY_CREATED_AT => $this->getCreatedAt()?->format('Y-m-d H:i:s'),
self::KEY_EXPIRES_AT => $this->getExpiresAt()?->format('Y-m-d H:i:s'),
self::KEY_IS_FEDERATED => [$this->isFederated(), PDO::PARAM_BOOL],
self::KEY_IS_FEDERATED => $this->isFederated(),
];
}

Expand Down
3 changes: 1 addition & 2 deletions src/Entities/RefreshTokenEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use DateTimeImmutable;
use League\OAuth2\Server\Entities\Traits\EntityTrait;
use League\OAuth2\Server\Entities\Traits\RefreshTokenTrait;
use PDO;
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\Traits\AssociateWithAuthCodeTrait;
Expand Down Expand Up @@ -52,7 +51,7 @@ public function getState(): array
'id' => $this->getIdentifier(),
'expires_at' => $this->getExpiryDateTime()->format('Y-m-d H:i:s'),
'access_token_id' => $this->getAccessToken()->getIdentifier(),
'is_revoked' => [$this->isRevoked(), PDO::PARAM_BOOL],
'is_revoked' => $this->isRevoked(),
'auth_code_id' => $this->getAuthCodeId(),
];
}
Expand Down
2 changes: 1 addition & 1 deletion src/Helpers/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function getFromRequest(
$clientId = empty($params['client_id']) ? null : (string)$params['client_id'];

if (!is_string($clientId)) {
throw new BadRequest('Client id is missing.');
throw new BadRequest('Client ID is missing.');
}

$client = $clientRepository->findById($clientId);
Expand Down
2 changes: 2 additions & 0 deletions src/Helpers/Random.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ public function getIdentifier(int $length = 40): string

try {
return bin2hex(random_bytes($length));
// @codeCoverageIgnoreStart
} catch (Throwable $e) {
throw OidcServerException::serverError('Could not generate a random string', $e);
}
// @codeCoverageIgnoreEnd
}
}
16 changes: 16 additions & 0 deletions src/ModuleConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class ModuleConfig
final public const OPTION_PROTOCOL_CACHE_ADAPTER = 'protocol_cache_adapter';
final public const OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS = 'protocol_cache_adapter_arguments';
final public const OPTION_PROTOCOL_USER_ENTITY_CACHE_DURATION = 'protocol_user_entity_cache_duration';
final public const OPTION_PROTOCOL_CLIENT_ENTITY_CACHE_DURATION = 'protocol_client_entity_cache_duration';
final public const OPTION_FEDERATION_PARTICIPATION_LIMIT_BY_TRUST_MARKS =
'federation_participation_limit_by_trust_marks';

Expand Down Expand Up @@ -464,6 +465,21 @@ public function getProtocolUserEntityCacheDuration(): DateInterval
);
}

/**
* Get cache duration for client entities (user data), with given default
*
* @throws \Exception
*/
public function getProtocolClientEntityCacheDuration(): DateInterval
{
return new DateInterval(
$this->config()->getOptionalString(
self::OPTION_PROTOCOL_CLIENT_ENTITY_CACHE_DURATION,
null,
) ?? 'PT10M',
);
}


/*****************************************************************************************************************
* OpenID Federation related config.
Expand Down
7 changes: 7 additions & 0 deletions src/Repositories/AbstractDatabaseRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,12 @@ public function __construct(
) {
}

public function getCacheKey(string $identifier): string
{
return is_string($tableName = $this->getTableName()) ?
$tableName . '_' . $identifier :
$identifier;
}

abstract public function getTableName(): ?string;
}
Loading
Loading