Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 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
932 changes: 932 additions & 0 deletions additional-tools/SSP-OIDC-module.postman_collection.json

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions additional-tools/generate-token.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
```
cat > /tmp/test.php << 'EOF'
<?php
require_once '/var/simplesamlphp/vendor/autoload.php';

use Lcobucci\JWT\Configuration;
use Lcobucci\JWT\Signer\Key\InMemory;
use Lcobucci\JWT\Signer\Rsa\Sha256;

// Database setup
$db = new PDO("sqlite:/var/simplesamlphp/data/mydb.sq3");

// Insert user with prepared statement
$stmt = $db->prepare("INSERT OR IGNORE INTO oidc_user (id, claims) VALUES (?, ?)");
$stmt->execute(["test-user-123", '{"sub":"test-user-123","name":"Test User"}']);

// Get first client
$client = $db->query("SELECT id FROM oidc_client LIMIT 1")->fetch(PDO::FETCH_ASSOC);
if (!$client) {
die("Error: No client found in database. Please create a client first.\n");
}
echo "Using client: " . $client["id"] . "\n";

// Generate token ID
$tokenId = "test-token-" . bin2hex(random_bytes(16));
echo "Token ID: " . $tokenId . "\n";

// Calculate expiration time
$now = new DateTimeImmutable();
$expiresAt = $now->modify('+1 hour');

// Insert access token into database
$stmt = $db->prepare("INSERT INTO oidc_access_token (id, scopes, expires_at, user_id, client_id, is_revoked) VALUES (?, ?, ?, ?, ?, 0)");
$stmt->execute([
$tokenId,
'["openid","profile","email"]',
$expiresAt->format('Y-m-d H:i:s'),
"test-user-123",
$client["id"]
]);

echo "✓ Token stored in database\n";

// Read the actual key contents
$privateKeyContents = file_get_contents('/var/simplesamlphp/cert/oidc_module.key');
$publicKeyContents = file_get_contents('/var/simplesamlphp/cert/oidc_module.crt');

// Configure JWT
$config = Configuration::forAsymmetricSigner(
new Sha256(),
InMemory::plainText($privateKeyContents),
InMemory::plainText($publicKeyContents)
);

// Build properly signed JWT with nbf claim
$token = $config->builder()
->issuedBy('https://localhost')
->permittedFor('resource-server')
->identifiedBy($tokenId)
->issuedAt($now)
->canOnlyBeUsedAfter($now) // ← ADD THIS: Sets the nbf (Not Before) claim
->expiresAt($expiresAt)
->relatedTo('test-user-123')
->withClaim('scopes', ['openid', 'profile', 'email'])
->withClaim('client_id', $client["id"])
->getToken($config->signer(), $config->signingKey());

$jwt = $token->toString();

// Verify the token can be parsed and validated
try {
$parsedToken = $config->parser()->parse($jwt);
$config->validator()->assert($parsedToken, ...$config->validationConstraints());
echo "✓ Token signature validated successfully!\n";
} catch (\Exception $e) {
echo "✗ Validation failed: " . $e->getMessage() . "\n";
}

// Output the JWT
echo "\n" . str_repeat("=", 80) . "\n";
echo "Generated JWT:\n";
echo str_repeat("=", 80) . "\n";
echo $jwt . "\n";
echo str_repeat("=", 80) . "\n";

// Provide test command
echo "\nTest with curl:\n";
echo "curl -X POST \"https://localhost/simplesaml/module.php/oidc/introspect\" \\\n";
echo " -H \"Content-Type: application/x-www-form-urlencoded\" \\\n";
echo " -H \"Authorization: Bearer test\" \\\n";
echo " -d \"token=$jwt\" \\\n";
echo " -k\n";

// Also show decoded token for debugging
echo "\n" . str_repeat("=", 80) . "\n";
echo "Decoded Token Payload:\n";
echo str_repeat("=", 80) . "\n";
$parts = explode('.', $jwt);
$payload = json_decode(base64_decode(strtr($parts[1], '-_', '+/')), true);
echo json_encode($payload, JSON_PRETTY_PRINT) . "\n";
EOF


php /tmp/test.php
```
24 changes: 23 additions & 1 deletion config/module_oidc.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -1037,10 +1037,27 @@ $config = [

/**
* (optional) Enable or disable API capabilities. Default is disabled
* (false).
* (false). If API capabilities are enabled, you can enable or disable
* specific API endpoints as needed and set up API tokens to allow
* access to those endpoints. If API capabilities are disabled, all API
* endpoints will be inaccessible regardless of the settings for
* specific endpoints and API tokens.
*
*/
ModuleConfig::OPTION_API_ENABLED => false,

/**
* (optional) API Enable VCI Credential Offer API endpoint. Default is
* disabled (false). Only relevant if API capabilities are enabled.
*/
ModuleConfig::OPTION_API_VCI_CREDENTIAL_OFFER_ENDPOINT_ENABLED => false,

/**
* (optional) API Enable OAuth2 Token Introspection API endpoint. Default
* is disabled (false). Only relevant if API capabilities are enabled.
*/
ModuleConfig::OPTION_API_OAUTH2_TOKEN_INTROSPECTION_ENDPOINT_ENABLED => false,

/**
* List of API tokens which can be used to access API endpoints based on
* given scopes. The format is: ['token' => [ApiScopesEnum]]
Expand All @@ -1050,6 +1067,11 @@ $config = [
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All, // Gives access to the whole API.
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciAll, // Gives access to all VCI-related endpoints.
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciCredentialOffer, // Gives access to the credential offer endpoint.
// ],
// 'strong-random-token-string-2' => [
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All, // Gives access to the whole API.
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::OAuth2All, // Gives access to all OAuth2-related endpoints.
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::OAuth2TokenIntrospection, // Gives access to the token introspection endpoint.
// ],
],
];
1 change: 1 addition & 0 deletions docker/ssp/config-override.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
'language.i18n.backend' => 'gettext/gettext',
'logging.level' => 7,
'usenewui' => false,
Comment thread
cicnavi marked this conversation as resolved.
Outdated

] + $config;
12 changes: 12 additions & 0 deletions docker/ssp/module_oidc.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,16 @@
ModuleConfig::OPTION_PROTOCOL_CACHE_ADAPTER_ARGUMENTS => [
// Use defaults
],

ModuleConfig::OPTION_API_ENABLED => true,

ModuleConfig::OPTION_API_VCI_CREDENTIAL_OFFER_ENDPOINT_ENABLED => true,

ModuleConfig::OPTION_API_OAUTH2_TOKEN_INTROSPECTION_ENDPOINT_ENABLED => true,

ModuleConfig::OPTION_API_TOKENS => [
'strong-random-token-string' => [
\SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::All, // Gives access to the whole API.
],
],
];
9 changes: 9 additions & 0 deletions docs/6-oidc-upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,15 @@
This is an upgrade guide from versions 1 → 7. Review the changes and
apply those relevant to your deployment.

In general, when upgrading any of the SimpleSAMLphp modules or the
SimpleSAMLphp instance itself, you should clear the SimpleSAMLphp
cache after the upgrade. In newer versions of SimpleSAMLphp, the
following command is available to do that:

```shell
composer clear-symfony-cache
```

## Version 6 to 7

As the database schema has been updated, you will have to run the DB migrations
Expand Down
7 changes: 7 additions & 0 deletions routing/routes/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use SimpleSAML\Module\oidc\Controllers\Federation\EntityStatementController;
use SimpleSAML\Module\oidc\Controllers\JwksController;
use SimpleSAML\Module\oidc\Controllers\OAuth2\OAuth2ServerConfigurationController;
use SimpleSAML\Module\oidc\Controllers\OAuth2\TokenIntrospectionController;
use SimpleSAML\Module\oidc\Controllers\UserInfoController;
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerConfigurationController;
use SimpleSAML\Module\oidc\Controllers\VerifiableCredentials\CredentialIssuerCredentialController;
Expand Down Expand Up @@ -142,4 +143,10 @@
RoutesEnum::ApiVciCredentialOffer->value,
)->controller([VciCredentialOfferApiController::class, 'credentialOffer'])
->methods([HttpMethodsEnum::POST->value]);

$routes->add(
RoutesEnum::ApiOAuth2TokenIntrospection->name,
RoutesEnum::ApiOAuth2TokenIntrospection->value,
)->controller(TokenIntrospectionController::class)
->methods([HttpMethodsEnum::POST->value]);
};
1 change: 1 addition & 0 deletions routing/services/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ services:
SimpleSAML\Module\oidc\Utils\RequestParamsResolver: ~
SimpleSAML\Module\oidc\Utils\ClassInstanceBuilder: ~
SimpleSAML\Module\oidc\Utils\JwksResolver: ~
SimpleSAML\Module\oidc\Utils\AuthenticatedOAuth2ClientResolver: ~
SimpleSAML\Module\oidc\Utils\ClaimTranslatorExtractor:
factory: ['@SimpleSAML\Module\oidc\Factories\ClaimTranslatorExtractorFactory', 'build']
SimpleSAML\Module\oidc\Utils\FederationCache:
Expand Down
66 changes: 66 additions & 0 deletions src/Bridges/OAuth2Bridge.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace SimpleSAML\Module\oidc\Bridges;

use Defuse\Crypto\Crypto;
use Defuse\Crypto\Key;
use SimpleSAML\Module\oidc\Exceptions\OidcException;
use SimpleSAML\Module\oidc\ModuleConfig;

class OAuth2Bridge
{
public function __construct(
protected readonly ModuleConfig $moduleConfig,
) {
}

/**
* Bridge `encrypt` function, which can be used instead of
* \League\OAuth2\Server\CryptTrait::encrypt()
*
* @param string $unencryptedData
* @param Key|string $encryptionKey
* @return string
* @throws OidcException
*/
public function encrypt(
string $unencryptedData,
null|Key|string $encryptionKey = null,
): string {
$encryptionKey ??= $this->moduleConfig->getEncryptionKey();

try {
return $encryptionKey instanceof Key ?
Crypto::encrypt($unencryptedData, $encryptionKey) :
Crypto::encryptWithPassword($unencryptedData, $encryptionKey);
} catch (\Exception $e) {
throw new OidcException('Error enrypting data: ' . $e->getMessage(), (int)$e->getCode(), $e);
}
}

/**
* Bridge `decrypt` function, which can be used instead of
* \League\OAuth2\Server\CryptTrait::decrypt()
*
* @param string $encryptedData
* @param Key|string $encryptionKey
* @return string
* @throws OidcException
*/
public function decrypt(
string $encryptedData,
null|Key|string $encryptionKey = null,
): string {
$encryptionKey ??= $this->moduleConfig->getEncryptionKey();

try {
return $encryptionKey instanceof Key ?
Crypto::decrypt($encryptedData, $encryptionKey) :
Crypto::decryptWithPassword($encryptedData, $encryptionKey);
} catch (\Exception $e) {
throw new OidcException('Error decrypting data: ' . $e->getMessage(), (int)$e->getCode(), $e);
}
}
}
4 changes: 4 additions & 0 deletions src/Codebooks/ApiScopesEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ enum ApiScopesEnum: string
// Verifiable Credential Issuance related scopes.
case VciAll = 'vci_all'; // Gives access to all VCI-related endpoints.
case VciCredentialOffer = 'vci_credential_offer'; // Gives access to the credential offer endpoint.

// OAuth2 related scopes.
case OAuth2All = 'oauth2_all'; // Gives access to all OAuth2-related endpoints.
case OAuth2TokenIntrospection = 'oauth2_token_introspection'; // Gives access to the token introspection endpoint.
}
1 change: 1 addition & 0 deletions src/Codebooks/RoutesEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ enum RoutesEnum: string
****************************************************************************************************************/

case ApiVciCredentialOffer = 'api/vci/credential-offer';
case ApiOAuth2TokenIntrospection = 'api/oauth2/token-introspection';
}
6 changes: 6 additions & 0 deletions src/Controllers/Api/VciCredentialOfferApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,15 @@ public function __construct(
}

/**
* @throws OidcServerException
*/
public function credentialOffer(Request $request): Response
{
if (!$this->moduleConfig->getApiVciCredentialOfferEndpointEnabled()) {
$this->loggerService->warning('Credential Offer API endpoint not enabled.');
throw OidcServerException::forbidden('Credential Offer API endpoint not enabled.');
}

$this->loggerService->debug('VciCredentialOfferApiController::credentialOffer');

$this->loggerService->debug(
Expand Down
Loading