Skip to content
Draft
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
9 changes: 9 additions & 0 deletions config/module_oidc.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,15 @@ $config = [
// claims for which "SAML attribute to OIDC claim translation" has been defined above.
ModuleConfig::OPTION_PROTOCOL_DISCOVERY_SHOW_CLAIMS_SUPPORTED => false,

// Optional security hardening (disabled by default). If enabled, confidential clients will be rejected with
// 'unauthorized_client' when they try to use the implicit flow (response_type 'id_token' or 'id_token token'),
// so they can only get tokens using the authorization code flow, where they authenticate at the token endpoint.
// The specifications do not forbid confidential clients from using the implicit flow, but OAuth 2.0 Security
// BCP advises against it (https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.2), and OIDC Core notes
// that confidential clients would normally use the authorization code flow
// (https://openid.net/specs/openid-connect-core-1_0.html#RPMTI).
ModuleConfig::OPTION_DISALLOW_IMPLICIT_FOR_CONFIDENTIAL_CLIENTS => false,

// Settings regarding Authentication Processing Filters.
// Note: OIDC authN state array will not contain all the keys which are available during SAML authN,
// like Service Provider metadata, etc.
Expand Down
1 change: 1 addition & 0 deletions src/Factories/Grant/ImplicitGrantFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public function build(): ImplicitGrant
$this->requestParamsResolver,
'#',
$this->accessTokenEntityFactory,
$this->moduleConfig->getDisallowImplicitForConfidentialClients(),
);
}
}
16 changes: 16 additions & 0 deletions src/ModuleConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ class ModuleConfig
final public const string OPTION_PROTOCOL_CLIENT_ENTITY_CACHE_DURATION = 'protocol_client_entity_cache_duration';
final public const string OPTION_PROTOCOL_DISCOVERY_SHOW_CLAIMS_SUPPORTED =
'protocol_discover_show_claims_supported';
final public const string OPTION_DISALLOW_IMPLICIT_FOR_CONFIDENTIAL_CLIENTS =
'disallow_implicit_for_confidential_clients';

final public const string OPTION_PKI_NEW_PRIVATE_KEY_PASSPHRASE = 'new_private_key_passphrase';
final public const string OPTION_PKI_NEW_PRIVATE_KEY_FILENAME = 'new_privatekey';
Expand Down Expand Up @@ -527,6 +529,20 @@ public function getProtocolDiscoveryShowClaimsSupported(): bool
);
}

/**
* Opt-in hardening: reject implicit flow requests from confidential clients.
*
* @see https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.2
* @see https://openid.net/specs/openid-connect-core-1_0.html#RPMTI
*/
public function getDisallowImplicitForConfidentialClients(): bool
{
return $this->config()->getOptionalBoolean(
self::OPTION_DISALLOW_IMPLICIT_FOR_CONFIDENTIAL_CLIENTS,
false,
);
}


/*****************************************************************************************************************
* OpenID Federation related config.
Expand Down
25 changes: 25 additions & 0 deletions src/Server/Exceptions/OidcServerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,31 @@ public static function unsupportedResponseType(
return $e;
}

/**
* Unauthorized client error.
*
* @see https://datatracker.ietf.org/doc/html/rfc6749#section-4.2.2.1
*
* @param string|null $hint
* @param string|null $redirectUri
* @param string|null $state
* @param bool $useFragment
* @return self
*/
public static function unauthorizedClient(
?string $hint = null,
?string $redirectUri = null,
?string $state = null,
bool $useFragment = false,
): self {
$errorMessage = 'The client is not authorized to request an authorization code or access token ' .
'using this method (unauthorized_client).';
$e = new self($errorMessage, 4, 'unauthorized_client', 400, $hint, $redirectUri, null, $state);
$e->useFragmentInHttpResponses($useFragment);

return $e;
}

/**
* Invalid scope error.
*
Expand Down
16 changes: 16 additions & 0 deletions src/Server/Grants/ImplicitGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function __construct(
protected RequestParamsResolver $requestParamsResolver,
protected string $queryDelimiter,
AccessTokenEntityFactory $accessTokenEntityFactory,
protected bool $disallowImplicitForConfidentialClients = false,
) {
parent::__construct($accessTokenTTL, $queryDelimiter);

Expand Down Expand Up @@ -144,6 +145,21 @@ public function validateAuthorizationRequestWithRequestRules(
/** @var \SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface $client */
$client = $resultBag->getOrFail(ClientIdRule::class)->getValue();

// Opt-in hardening (ModuleConfig::OPTION_DISALLOW_IMPLICIT_FOR_CONFIDENTIAL_CLIENTS, default false).
// The specs do not forbid confidential clients from using the implicit flow, but the implicit flow never
// authenticates the client, and OAuth 2.0 Security BCP advises against it:
// https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1.2
// OIDC Core notes that confidential clients would normally use the authorization code flow:
// https://openid.net/specs/openid-connect-core-1_0.html#RPMTI
if ($this->disallowImplicitForConfidentialClients && $client->isConfidential()) {
throw OidcServerException::unauthorizedClient(
'Confidential clients are not allowed to use the implicit grant.',
$redirectUri,
$state,
$this->shouldUseFragment(),
);
}

// Some rules need certain things available in order to work properly...
$this->requestRulesManager->setData('default_scope', $this->defaultScope);
$this->requestRulesManager->setData('scope_delimiter_string', self::SCOPE_DELIMITER_STRING);
Expand Down
2 changes: 1 addition & 1 deletion src/Server/Grants/RefreshTokenGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ protected function validateOldRefreshToken(ServerRequestInterface $request, $cli

protected function issueRefreshToken(
OAuth2AccessTokenEntityInterface $accessToken,
string $authCodeId = null,
?string $authCodeId = null,
): ?RefreshTokenEntityInterface {
if (! is_a($accessToken, AccessTokenEntityInterface::class)) {
throw OidcServerException::serverError('Unexpected access token entity type.');
Expand Down
17 changes: 16 additions & 1 deletion tests/bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
*/

$projectRoot = dirname(__DIR__);
require_once $projectRoot . '/vendor/autoload.php';
if (file_exists($projectRoot . '/vendor/autoload.php')) {
require_once $projectRoot . '/vendor/autoload.php';
} else {
require_once dirname(__DIR__, 3) . '/vendor/autoload.php';
}

// Symlink module into ssp vendor lib so that templates and urls can resolve correctly
$linkPath = $projectRoot . '/vendor/simplesamlphp/simplesamlphp/modules/oidc';
Expand All @@ -25,3 +29,14 @@
} else {
echo "'$linkPath' linked to '$projectRoot'\n";
}

$certDir = $projectRoot . '/vendor/simplesamlphp/simplesamlphp/cert';
$mainCertDir = dirname(__DIR__, 3) . '/cert';
if (file_exists($mainCertDir) && file_exists($certDir)) {
foreach (glob($mainCertDir . '/*') as $certFile) {
$dest = $certDir . '/' . basename($certFile);
if (!file_exists($dest)) {
copy($certFile, $dest);
}
}
}
Loading
Loading