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
2 changes: 2 additions & 0 deletions docker/conformance.sql
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ CREATE TABLE oidc_access_token (
authorization_details TEXT NULL,
bound_client_id TEXT NULL,
bound_redirect_uri TEXT NULL,
issuer_state TEXT NULL,
CONSTRAINT FK_43C1650EA76ED395 FOREIGN KEY (user_id)
REFERENCES oidc_user (id) ON DELETE CASCADE,
CONSTRAINT FK_43C1650E19EB6921 FOREIGN KEY (client_id)
Expand Down Expand Up @@ -92,6 +93,7 @@ CREATE TABLE oidc_auth_code (
authorization_details TEXT NULL,
bound_client_id TEXT NULL,
bound_redirect_uri TEXT NULL,
issuer_state TEXT NULL,
CONSTRAINT FK_97D32CA7A76ED395 FOREIGN KEY (user_id)
REFERENCES oidc_user (id) ON DELETE CASCADE,
CONSTRAINT FK_97D32CA719EB6921 FOREIGN KEY (client_id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use SimpleSAML\Module\oidc\Entities\AccessTokenEntity;
use SimpleSAML\Module\oidc\ModuleConfig;
use SimpleSAML\Module\oidc\Repositories\AccessTokenRepository;
use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository;
use SimpleSAML\Module\oidc\Repositories\UserRepository;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
use SimpleSAML\Module\oidc\Services\LoggerService;
Expand Down Expand Up @@ -53,6 +54,7 @@ public function __construct(
protected readonly RequestParamsResolver $requestParamsResolver,
protected readonly UserRepository $userRepository,
protected readonly Did $did,
protected readonly IssuerStateRepository $issuerStateRepository,
) {
if (!$this->moduleConfig->getVerifiableCredentialEnabled()) {
$this->loggerService->warning('Verifiable Credential capabilities not enabled.');
Expand Down Expand Up @@ -121,6 +123,30 @@ public function credential(Request $request): Response
);
}

$issuerState = $accessToken->getIssuerState();
if (!is_string($issuerState)) {
$this->loggerService->error(
'CredentialIssuerCredentialController::credential: Issuer state missing in access token.',
['access_token' => $accessToken],
);
return $this->routes->newJsonErrorResponse(
'invalid_token',
'Issuer state missing in access token.',
401,
);
}

if ($this->issuerStateRepository->findValid($issuerState) === null) {
$this->loggerService->warning(
'CredentialIssuerCredentialController::credential: Issuer state not valid.',
['issuer_state' => $issuerState],
);
return $this->routes->newJsonErrorResponse(
'invalid_token',
'Issuer state not valid.',
);
}

if (
isset($requestData[ClaimsEnum::CredentialConfigurationId->value]) &&
isset($requestData[ClaimsEnum::CredentialIdentifier->value])
Expand Down Expand Up @@ -652,7 +678,11 @@ public function credential(Request $request): Response
throw new OpenIdException('Invalid credential format ID.');
}

$this->loggerService->debug('response', [
$this->loggerService->debug('Revoking issuer state.', ['issuerState' => $issuerState]);
;
$this->issuerStateRepository->revoke($issuerState);

$this->loggerService->debug('Returning credential response.', [
'credentials' => [
['credential' => $verifiableCredential->getToken()],
],
Expand Down
10 changes: 10 additions & 0 deletions src/Entities/AccessTokenEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public function __construct(
protected readonly ?array $authorizationDetails = null,
protected readonly ?string $boundClientId = null,
protected readonly ?string $boundRedirectUri = null,
protected readonly ?string $issuerState = null,
) {
$this->setIdentifier($id);
$this->setClient($clientEntity);
Expand Down Expand Up @@ -125,6 +126,7 @@ public function getState(): array
null,
'bound_client_id' => $this->boundClientId,
'bound_redirect_uri' => $this->boundRedirectUri,
'issuer_state' => $this->issuerState,
];
}

Expand Down Expand Up @@ -166,6 +168,9 @@ protected function convertToJWT(): Token
->expiresAt($this->getExpiryDateTime())
->relatedTo((string) $this->getUserIdentifier())
->withClaim('scopes', $this->getScopes());
if ($this->issuerState !== null) {
$jwtBuilder = $jwtBuilder->withClaim('issuer_state', $this->issuerState);
}

return $this->jsonWebTokenBuilderService->getSignedProtocolJwt($jwtBuilder);
}
Expand All @@ -189,4 +194,9 @@ public function getBoundRedirectUri(): ?string
{
return $this->boundRedirectUri;
}

public function getIssuerState(): ?string
{
return $this->issuerState;
}
}
7 changes: 7 additions & 0 deletions src/Entities/AuthCodeEntity.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public function __construct(
protected readonly ?array $authorizationDetails = null,
protected readonly ?string $boundClientId = null,
protected readonly ?string $boundRedirectUri = null,
protected readonly ?string $issuerState = null,
) {
$this->identifier = $id;
$this->client = $client;
Expand Down Expand Up @@ -81,6 +82,7 @@ public function getState(): array
null,
'bound_client_id' => $this->boundClientId,
'bound_redirect_uri' => $this->boundRedirectUri,
'issuer_state' => $this->issuerState,
];
}

Expand Down Expand Up @@ -113,4 +115,9 @@ public function getBoundRedirectUri(): ?string
{
return $this->boundRedirectUri;
}

public function getIssuerState(): ?string
{
return $this->issuerState;
}
}
2 changes: 2 additions & 0 deletions src/Factories/Entities/AccessTokenEntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function fromData(
?array $authorizationDetails = null,
?string $boundClientId = null,
?string $boundRedirectUri = null,
?string $issuerState = null,
): AccessTokenEntity {
return new AccessTokenEntity(
$id,
Expand All @@ -56,6 +57,7 @@ public function fromData(
authorizationDetails: $authorizationDetails,
boundClientId: $boundClientId,
boundRedirectUri: $boundRedirectUri,
issuerState: $issuerState,
);
}

Expand Down
4 changes: 4 additions & 0 deletions src/Factories/Entities/AuthCodeEntityFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public function fromData(
?string $userIdentifier = null,
?string $redirectUri = null,
?string $nonce = null,
?string $issuerState = null,
bool $isRevoked = false,
?FlowTypeEnum $flowTypeEnum = null,
?string $txCode = null,
Expand All @@ -52,6 +53,7 @@ public function fromData(
$authorizationDetails,
$boundClientId,
$boundRedirectUri,
$issuerState,
);
}

Expand Down Expand Up @@ -94,6 +96,7 @@ public function fromState(array $state): AuthCodeEntity
$isRevoked = (bool) $state['is_revoked'];
$flowType = empty($state['flow_type']) ? null : FlowTypeEnum::tryFrom((string)$state['flow_type']);
$txCode = empty($state['tx_code']) ? null : (string)$state['tx_code'];
$issuerState = empty($state['issuer_state']) ? null : (string)$state['issuer_state'];

/** @psalm-suppress MixedAssignment */
$authorizationDetails = isset($state['authorization_details']) && is_string($state['authorization_details']) ?
Expand All @@ -112,6 +115,7 @@ public function fromState(array $state): AuthCodeEntity
$userIdentifier,
$redirectUri,
$nonce,
$issuerState,
$isRevoked,
$flowType,
$txCode,
Expand Down
4 changes: 1 addition & 3 deletions src/Factories/RequestRulesManagerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use SimpleSAML\Module\oidc\ModuleConfig;
use SimpleSAML\Module\oidc\Repositories\ClientRepository;
use SimpleSAML\Module\oidc\Repositories\CodeChallengeVerifiersRepository;
use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository;
use SimpleSAML\Module\oidc\Repositories\ScopeRepository;
use SimpleSAML\Module\oidc\Server\RequestRules\RequestRulesManager;
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\AcrValuesRule;
Expand Down Expand Up @@ -66,7 +65,6 @@ public function __construct(
private readonly JwksResolver $jwksResolver,
private readonly FederationParticipationValidator $federationParticipationValidator,
private readonly SspBridge $sspBridge,
private readonly IssuerStateRepository $issuerStateRepository,
private readonly ?FederationCache $federationCache = null,
private readonly ?ProtocolCache $protocolCache = null,
) {
Expand All @@ -89,6 +87,7 @@ private function getDefaultRules(): array
{
return [
new StateRule($this->requestParamsResolver, $this->helpers),
new IssuerStateRule($this->requestParamsResolver, $this->helpers),
new ClientRule(
$this->requestParamsResolver,
$this->helpers,
Expand Down Expand Up @@ -147,7 +146,6 @@ private function getDefaultRules(): array
$this->protocolCache,
),
new CodeVerifierRule($this->requestParamsResolver, $this->helpers),
new IssuerStateRule($this->requestParamsResolver, $this->helpers, $this->issuerStateRepository),
new AuthorizationDetailsRule($this->requestParamsResolver, $this->helpers, $this->moduleConfig),
new ClientIdRule($this->requestParamsResolver, $this->helpers),
];
Expand Down
8 changes: 5 additions & 3 deletions src/Repositories/AccessTokenRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTo
flow_type,
authorization_details,
bound_client_id,
bound_redirect_uri
bound_redirect_uri,
issuer_state
) "
. "VALUES (
:id,
Expand All @@ -127,7 +128,8 @@ public function persistNewAccessToken(OAuth2AccessTokenEntityInterface $accessTo
:flow_type,
:authorization_details,
:bound_client_id,
:bound_redirect_uri
:bound_redirect_uri,
:issuer_state
)",
$this->getTableName(),
);
Expand Down Expand Up @@ -267,7 +269,7 @@ private function update(AccessTokenEntity $accessTokenEntity): void
. "client_id = :client_id, is_revoked = :is_revoked, auth_code_id = :auth_code_id, "
. "requested_claims = :requested_claims, flow_type = :flow_type, " .
"authorization_details = :authorization_details, bound_client_id = :bound_client_id, " .
"bound_redirect_uri = :bound_redirect_uri WHERE id = :id",
"bound_redirect_uri = :bound_redirect_uri, issuer_state = :issuer_state WHERE id = :id",
$this->getTableName(),
);

Expand Down
9 changes: 6 additions & 3 deletions src/Repositories/AuthCodeRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public function persistNewAuthCode(OAuth2AuthCodeEntityInterface $authCodeEntity
tx_code,
authorization_details,
bound_client_id,
bound_redirect_uri
bound_redirect_uri,
issuer_state
) VALUES (
:id,
:scopes,
Expand All @@ -98,7 +99,8 @@ public function persistNewAuthCode(OAuth2AuthCodeEntityInterface $authCodeEntity
:tx_code,
:authorization_details,
:bound_client_id,
:bound_redirect_uri
:bound_redirect_uri,
:issuer_state
)
EOS,
$this->getTableName(),
Expand Down Expand Up @@ -224,7 +226,8 @@ private function update(AuthCodeEntity $authCodeEntity): void
tx_code = :tx_code,
authorization_details = :authorization_details,
bound_client_id = :bound_client_id,
bound_redirect_uri = :bound_redirect_uri
bound_redirect_uri = :bound_redirect_uri,
issuer_state = :issuer_state
WHERE id = :id
EOS
,
Expand Down
25 changes: 25 additions & 0 deletions src/Server/Grants/AuthCodeGrant.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use SimpleSAML\Module\oidc\Entities\Interfaces\AccessTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\AuthCodeEntityInterface;
use SimpleSAML\Module\oidc\Entities\Interfaces\RefreshTokenEntityInterface;
use SimpleSAML\Module\oidc\Entities\ScopeEntity;
use SimpleSAML\Module\oidc\Entities\UserEntity;
use SimpleSAML\Module\oidc\Factories\Entities\AccessTokenEntityFactory;
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
Expand Down Expand Up @@ -347,6 +348,7 @@ protected function issueOidcAuthCode(
$userIdentifier,
$redirectUri,
$authorizationRequest->getNonce(),
$authorizationRequest->getIssuerState(),
flowTypeEnum: $flowType,
authorizationDetails: $authorizationRequest->getAuthorizationDetails(),
boundClientId: $authorizationRequest->getBoundClientId(),
Expand Down Expand Up @@ -615,6 +617,7 @@ public function respondToAccessTokenRequest(
$storedAuthCodeEntity->getAuthorizationDetails(),
$storedAuthCodeEntity->getBoundClientId(),
$storedAuthCodeEntity->getBoundRedirectUri(),
$storedAuthCodeEntity->getIssuerState(),
);
$this->getEmitter()->emit(new RequestEvent(RequestEvent::ACCESS_TOKEN_ISSUED, $request));
$responseType->setAccessToken($accessToken);
Expand Down Expand Up @@ -893,6 +896,28 @@ public function validateAuthorizationRequestWithRequestRules(
);
$authorizationRequest->setAuthorizationDetails($authorizationDetails);

// TODO This is a band-aid fix for having credential claims in the userinfo endpoint when
// only VCI authorizationDetails are supplied. This requires configuring a matching OIDC scope
// that has all the credential type claims as well.
if (is_array($authorizationDetails)) {
/** @psalm-suppress MixedAssignment */
foreach ($authorizationDetails as $authorizationDetail) {
if (
is_array($authorizationDetail) &&
(isset($authorizationDetail['type'])) &&
($authorizationDetail['type']) === 'openid_credential'
) {
/** @psalm-suppress MixedAssignment */
$credentialConfigurationId = $authorizationDetail['credential_configuration_id'] ?? null;
if (is_string($credentialConfigurationId)) {
$scopes[] = new ScopeEntity($credentialConfigurationId);
}
}
}
$this->loggerService->debug('authorizationDetails Resolved Scopes: ', ['scopes' => $scopes]);
$authorizationRequest->setScopes($scopes);
}

// Check if we are using a generic client for this request. This can happen for non-registered clients
// in VCI flows. This can be removed once the VCI clients (wallets) are properly registered using DCR.
if ($client->isGeneric()) {
Expand Down
2 changes: 2 additions & 0 deletions src/Server/Grants/Traits/IssueAccessTokenTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ protected function issueAccessToken(
?array $authorizationDetails = null,
?string $boundClientId = null,
?string $boundRedirectUri = null,
?string $issuerState = null,
): AccessTokenEntityInterface {
$maxGenerationAttempts = AbstractGrant::MAX_RANDOM_TOKEN_GENERATION_ATTEMPTS;

Expand All @@ -79,6 +80,7 @@ protected function issueAccessToken(
authorizationDetails: $authorizationDetails,
boundClientId: $boundClientId,
boundRedirectUri: $boundRedirectUri,
issuerState: $issuerState,
);
$this->accessTokenRepository->persistNewAccessToken($accessToken);
return $accessToken;
Expand Down
25 changes: 0 additions & 25 deletions src/Server/RequestRules/Rules/IssuerStateRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,15 @@
namespace SimpleSAML\Module\oidc\Server\RequestRules\Rules;

use Psr\Http\Message\ServerRequestInterface;
use SimpleSAML\Module\oidc\Helpers;
use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository;
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
use SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultBagInterface;
use SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultInterface;
use SimpleSAML\Module\oidc\Server\RequestRules\Result;
use SimpleSAML\Module\oidc\Services\LoggerService;
use SimpleSAML\Module\oidc\Utils\RequestParamsResolver;
use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum;
use SimpleSAML\OpenID\Codebooks\ParamsEnum;

class IssuerStateRule extends AbstractRule
{
public function __construct(
RequestParamsResolver $requestParamsResolver,
Helpers $helpers,
protected readonly IssuerStateRepository $issuerStateRepository,
) {
parent::__construct($requestParamsResolver, $helpers);
}

/**
* @inheritDoc
*/
Expand All @@ -37,25 +25,12 @@ public function checkRule(
bool $useFragmentInHttpErrorResponses = false,
array $allowedServerRequestMethods = [HttpMethodsEnum::GET],
): ?ResultInterface {
$loggerService->debug('IssuerStateRule::checkRule');

$issuerState = $this->requestParamsResolver->getAsStringBasedOnAllowedMethods(
ParamsEnum::IssuerState->value,
$request,
$allowedServerRequestMethods,
);

if ($issuerState === null) {
return null;
}

if ($this->issuerStateRepository->findValid($issuerState) === null) {
$loggerService->error('IssuerStateRule: Invalid issuer state: ' . $issuerState);
throw OidcServerException::invalidRequest(ParamsEnum::IssuerState->value);
}

$loggerService->debug('IssuerStateRule: Valid issuer state: ' . $issuerState);

return new Result($this->getKey(), $issuerState);
}
}
Loading