diff --git a/composer.json b/composer.json
index 6bac59e6..8a91b03b 100644
--- a/composer.json
+++ b/composer.json
@@ -35,7 +35,7 @@
"spomky-labs/base64url": "^2.0",
"symfony/expression-language": "^7.4",
"symfony/psr-http-message-bridge": "^7.4",
- "web-token/jwt-framework": "^3",
+ "web-token/jwt-framework": "^3.4.10",
"symfony/cache": "^7.4",
"psr/simple-cache": "^3"
},
diff --git a/docs/3-oidc-configuration.md b/docs/3-oidc-configuration.md
index 12ad22c7..1222d94d 100644
--- a/docs/3-oidc-configuration.md
+++ b/docs/3-oidc-configuration.md
@@ -13,6 +13,7 @@ It complements the inline comments in `config/module_oidc.php`.
- Attribute translation
- Auth Proc filters (OIDC)
- Client registration permissions
+- Running multiple OPs on one server
## Caching protocol artifacts
@@ -218,3 +219,73 @@ $config = [
Users can visit the following link for administration:
- [https://example.com/simplesaml/module.php/oidc/clients/](https://example.com/simplesaml/module.php/oidc/clients/)
+
+## Running multiple OPs on one server
+
+A single module instance is designed to serve exactly one OpenID Provider
+(OP): it has one issuer, one set of signing keys, and one configuration
+file (`module_oidc.php`). If you need to run several independent OPs (each
+with its own issuer, keys, clients, and scopes) on the same server, do not
+try to fit them into one config. Instead, run multiple SimpleSAMLphp
+instances side by side and select between them with the
+`SIMPLESAMLPHP_CONFIG_DIR` environment variable.
+
+The idea is to give each OP its own configuration directory (each with its
+own `config.php`, `authsources.php`, `module_oidc.php`, signing keys, and
+metadata) and to front each one with its own virtual host. SimpleSAMLphp
+reads `SIMPLESAMLPHP_CONFIG_DIR` to decide which configuration directory to
+load, so each virtual host points at the configuration for its OP:
+
+```apache
+# Virtual host for the first OP
+
+ ServerName op1.example.org
+ SetEnv SIMPLESAMLPHP_CONFIG_DIR /etc/simplesamlphp/op1/config
+ # ... remaining SimpleSAMLphp / web root configuration ...
+
+
+# Virtual host for the second OP
+
+ ServerName op2.example.org
+ SetEnv SIMPLESAMLPHP_CONFIG_DIR /etc/simplesamlphp/op2/config
+ # ... remaining SimpleSAMLphp / web root configuration ...
+
+```
+
+With nginx + PHP-FPM, set the same variable per server block via
+`fastcgi_param SIMPLESAMLPHP_CONFIG_DIR /etc/simplesamlphp/op1/config;`
+(or use a separate PHP-FPM pool per OP with `env[SIMPLESAMLPHP_CONFIG_DIR]`).
+
+In each OP's `module_oidc.php`, set a distinct `issuer` and distinct signing
+key/certificate filenames so the OPs do not share identities or keys.
+
+### Important: isolate the database (or use a table prefix)
+
+The OIDC module keeps its protocol artifacts — clients, access tokens,
+refresh tokens, authorization codes, allowed origins, and user records — in
+the database, and these tables have no notion of which OP they belong to.
+If two instances point at the same database tables, they will share all of
+that state: a client registered on one OP will be visible to the other, and
+the admin UIs will operate on the same data. That is almost certainly not
+what you want.
+
+To keep the OPs properly isolated, give each instance separate storage by
+configuring its `config.php` to use **either** a separate database **or** a
+distinct table prefix:
+
+```php
+// In op1/config/config.php
+'database.dsn' => 'mysql:host=localhost;dbname=ssp_oidc_op1',
+// ...or share a database but separate the tables with a distinct prefix:
+'database.prefix' => 'op1_',
+```
+
+```php
+// In op2/config/config.php
+'database.dsn' => 'mysql:host=localhost;dbname=ssp_oidc_op2',
+// ...or:
+'database.prefix' => 'op2_',
+```
+
+Run the database schema creation (migrations) for each instance separately,
+so each OP gets its own set of tables.
diff --git a/src/Server/LogoutHandlers/BackChannelLogoutHandler.php b/src/Server/LogoutHandlers/BackChannelLogoutHandler.php
index e1fb8478..9a0460e2 100644
--- a/src/Server/LogoutHandlers/BackChannelLogoutHandler.php
+++ b/src/Server/LogoutHandlers/BackChannelLogoutHandler.php
@@ -39,13 +39,13 @@ public function handle(array $relyingPartyAssociations, ?HandlerStack $handlerSt
'concurrency' => 5,
'fulfilled' => function (Response $response, mixed $index) {
// this is delivered each successful response
- $successMessage = "Backhannel Logout (index $index) - success, status: {$response->getStatusCode()} " .
+ $successMessage = "Backchannel Logout (index $index) - success, status: {$response->getStatusCode()} " .
"{$response->getReasonPhrase()}";
$this->loggerService->notice($successMessage);
},
'rejected' => function (GuzzleException $reason, mixed $index) {
// this is delivered each failed request
- $errorMessage = "Backhannel Logout (index $index) - error, reason: {$reason->getCode()} " .
+ $errorMessage = "Backchannel Logout (index $index) - error, reason: {$reason->getCode()} " .
"{$reason->getMessage()}, exception type: " . $reason::class;
$this->loggerService->error($errorMessage);
},