Skip to content

Commit 36553ad

Browse files
committed
WIP
1 parent 2dd5315 commit 36553ad

File tree

8 files changed

+437
-2
lines changed

8 files changed

+437
-2
lines changed

config/module_oidc.php.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,4 +823,9 @@ $config = [
823823
// \SimpleSAML\Module\oidc\Codebooks\ApiScopesEnum::VciCredentialOffer, // Gives access to the credential offer endpoint.
824824
// ],
825825
],
826+
827+
// (optional) Issuer State TTL (validity duration), with the given example. If not set, falls back to
828+
// Authorization Code TTL. For duration format info, check
829+
// https://www.php.net/manual/en/dateinterval.construct.php
830+
ModuleConfig::OPTION_ISSUER_STATE_TTL => 'PT10M', // 10 minutes
826831
];

hooks/hook_cron.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use SimpleSAML\Module\oidc\ModuleConfig;
1919
use SimpleSAML\Module\oidc\Repositories\AccessTokenRepository;
2020
use SimpleSAML\Module\oidc\Repositories\AuthCodeRepository;
21+
use SimpleSAML\Module\oidc\Repositories\IssuerStateRepository;
2122
use SimpleSAML\Module\oidc\Repositories\RefreshTokenRepository;
2223
use SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException;
2324
use SimpleSAML\Module\oidc\Services\Container;
@@ -64,6 +65,10 @@ function oidc_hook_cron(array &$croninfo): void
6465
$refreshTokenRepository = $container->get(RefreshTokenRepository::class);
6566
$refreshTokenRepository->removeExpired();
6667

68+
/** @var \SimpleSAML\Module\oidc\Repositories\IssuerStateRepository $issuerStateRepository */
69+
$issuerStateRepository = $container->get(IssuerStateRepository::class);
70+
$issuerStateRepository->removeInvalid();
71+
6772
$croninfo['summary'][] = 'Module `oidc` clean up. Removed expired entries from storage.';
6873
} catch (Exception $e) {
6974
$message = 'Module `oidc` clean up cron script failed: ' . $e->getMessage();

src/Entities/IssuerStateEntity.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Entities;
6+
7+
use DateTimeImmutable;
8+
use SimpleSAML\Module\oidc\Entities\Interfaces\MementoInterface;
9+
10+
/**
11+
* @psalm-suppress PropertyNotSetInConstructor
12+
*/
13+
class IssuerStateEntity implements MementoInterface
14+
{
15+
public function __construct(
16+
protected readonly string $value,
17+
protected readonly DateTimeImmutable $createdAt,
18+
protected readonly DateTimeImmutable $expirestAt,
19+
protected bool $isRevoked = false,
20+
) {
21+
}
22+
23+
public function getState(): array
24+
{
25+
return [
26+
'value' => $this->getValue(),
27+
'created_at' => $this->getCreatedAt()->format('Y-m-d H:i:s'),
28+
'expires_at' => $this->getExpirestAt()->format('Y-m-d H:i:s'),
29+
'is_revoked' => $this->isRevoked(),
30+
];
31+
}
32+
33+
public function getValue(): string
34+
{
35+
return $this->value;
36+
}
37+
38+
public function getCreatedAt(): DateTimeImmutable
39+
{
40+
return $this->createdAt;
41+
}
42+
43+
public function getExpirestAt(): DateTimeImmutable
44+
{
45+
return $this->expirestAt;
46+
}
47+
48+
public function isRevoked(): bool
49+
{
50+
return $this->isRevoked;
51+
}
52+
53+
public function revoke(): void
54+
{
55+
$this->isRevoked = true;
56+
}
57+
}

src/Factories/CredentialOfferUriFactory.php

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use SimpleSAML\Module\oidc\Entities\UserEntity;
1414
use SimpleSAML\Module\oidc\Factories\Entities\AuthCodeEntityFactory;
1515
use SimpleSAML\Module\oidc\Factories\Entities\ClientEntityFactory;
16+
use SimpleSAML\Module\oidc\Factories\Entities\IssuerStateEntityFactory;
1617
use SimpleSAML\Module\oidc\Factories\Entities\UserEntityFactory;
1718
use SimpleSAML\Module\oidc\ModuleConfig;
1819
use SimpleSAML\Module\oidc\Repositories\AuthCodeRepository;
@@ -21,6 +22,7 @@
2122
use SimpleSAML\Module\oidc\Services\LoggerService;
2223
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
2324
use SimpleSAML\OpenID\Codebooks\GrantTypesEnum;
25+
use SimpleSAML\OpenID\Exceptions\OpenIdException;
2426
use SimpleSAML\OpenID\VerifiableCredentials;
2527
use SimpleSAML\OpenID\VerifiableCredentials\TxCode;
2628

@@ -38,9 +40,59 @@ public function __construct(
3840
protected readonly UserRepository $userRepository,
3941
protected readonly UserEntityFactory $userEntityFactory,
4042
protected readonly EmailFactory $emailFactory,
43+
protected readonly IssuerStateEntityFactory $issuerStateEntityFactory,
4144
) {
4245
}
4346

47+
/**
48+
* @param string[] $credentialConfigurationIds
49+
* @throws \SimpleSAML\OpenId\Exceptions\OpenIdException
50+
*/
51+
public function buildForAuthorization(
52+
array $credentialConfigurationIds,
53+
): string {
54+
55+
$issuerState = null;
56+
57+
$issuerStateGenerationAttempts = 3;
58+
while ($issuerStateGenerationAttempts > 0) {
59+
$newIssuerState = $this->issuerStateEntityFactory->buildNew();
60+
if ($this->authCodeRepository->findById($newIssuerState->getValue()) === null) {
61+
$issuerState = $newIssuerState;
62+
break;
63+
}
64+
$issuerStateGenerationAttempts--;
65+
}
66+
67+
if ($issuerState === null) {
68+
throw new OpenIdException('Failed to generate issuer state.');
69+
}
70+
71+
72+
$credentialOffer = $this->verifiableCredentials->credentialOfferFactory()->from(
73+
parameters: [
74+
ClaimsEnum::CredentialIssuer->value => $this->moduleConfig->getIssuer(),
75+
ClaimsEnum::CredentialConfigurationIds->value => [
76+
...$credentialConfigurationIds,
77+
],
78+
ClaimsEnum::Grants->value => [
79+
GrantTypesEnum::AuthorizationCode->value => [
80+
ClaimsEnum::IssuerState->value => $issuerState->getValue(),
81+
],
82+
],
83+
],
84+
);
85+
86+
$credentialOfferValue = $credentialOffer->jsonSerialize();
87+
$parameterName = ParametersEnum::CredentialOfferUri->value;
88+
if (is_array($credentialOfferValue)) {
89+
$parameterName = ParametersEnum::CredentialOffer->value;
90+
$credentialOfferValue = json_encode($credentialOfferValue);
91+
}
92+
93+
return "openid-credential-offer://?$parameterName=$credentialOfferValue";
94+
}
95+
4496
/**
4597
* @param string[] $credentialConfigurationIds
4698
* @throws \SimpleSAML\OpenId\Exceptions\OpenIdException
@@ -119,8 +171,9 @@ public function buildPreAuthorized(
119171
$authCodeId = null;
120172
$authCodeIdGenerationAttempts = 3;
121173
while ($authCodeIdGenerationAttempts > 0) {
122-
$authCodeId = $this->sspBridge->utils()->random()->generateID();
123-
if ($this->authCodeRepository->findById($authCodeId) === null) {
174+
$newAuthCodeId = $this->sspBridge->utils()->random()->generateID();
175+
if ($this->authCodeRepository->findById($newAuthCodeId) === null) {
176+
$authCodeId = $newAuthCodeId;
124177
break;
125178
}
126179
$authCodeIdGenerationAttempts--;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Module\oidc\Factories\Entities;
6+
7+
use DateTimeImmutable;
8+
use SimpleSAML\Module\oidc\Entities\IssuerStateEntity;
9+
use SimpleSAML\Module\oidc\Helpers;
10+
use SimpleSAML\Module\oidc\Helpers\Random;
11+
use SimpleSAML\Module\oidc\ModuleConfig;
12+
use SimpleSAML\OpenID\Exceptions\OpenIdException;
13+
14+
class IssuerStateEntityFactory
15+
{
16+
public function __construct(
17+
protected readonly ModuleConfig $moduleConfig,
18+
protected readonly Random $random,
19+
protected readonly Helpers $helpers,
20+
) {
21+
}
22+
23+
/**
24+
* @throws OpenIdException
25+
* @throws OidcServerException
26+
* @throws \Exception
27+
*/
28+
public function buildNew(
29+
?string $value = null,
30+
?DateTimeImmutable $createdAt = null,
31+
?DateTimeImmutable $expiresAt = null,
32+
bool $isRevoked = false,
33+
): IssuerStateEntity {
34+
$value ??= hash('sha256', $this->helpers->random()->getIdentifier());
35+
36+
$createdAt ??= $this->helpers->dateTime()->getUtc();
37+
$expiresAt ??= $createdAt->add($this->moduleConfig->getIssuerStateDuration());
38+
39+
return $this->fromData($value, $createdAt, $expiresAt, $isRevoked);
40+
}
41+
42+
/**
43+
* @param string $value Issuer State Entity value, max 64 characters.
44+
* @throws OpenIdException
45+
*/
46+
public function fromData(
47+
string $value,
48+
DateTimeImmutable $createdAt,
49+
DateTimeImmutable $expiresAt,
50+
bool $isRevoked = false,
51+
): IssuerStateEntity {
52+
if (strlen($value) > 64) {
53+
throw new OpenIdException('Invalid Issuer State Entity value.');
54+
}
55+
56+
return new IssuerStateEntity($value, $createdAt, $expiresAt, $isRevoked);
57+
}
58+
59+
/**
60+
* @param mixed[] $state
61+
* @return IssuerStateEntity
62+
* @throws OpenIdException
63+
*/
64+
public function fromState(array $state): IssuerStateEntity
65+
{
66+
if (
67+
!is_string($value = $state['value']) ||
68+
!is_string($createdAt = $state['created_at']) ||
69+
!is_string($expiresAt = $state['expires_at'])
70+
) {
71+
throw new OpenIdException('Invalid Issuer State Entity state.');
72+
}
73+
74+
if (strlen($value) > 64) {
75+
throw new OpenIdException('Invalid Issuer State Entity value.');
76+
}
77+
78+
$isRevoked = (bool)($state['is_revoked'] ?? true);
79+
80+
return new IssuerStateEntity(
81+
$value,
82+
$this->helpers->dateTime()->getUtc($createdAt),
83+
$this->helpers->dateTime()->getUtc($expiresAt),
84+
$isRevoked,
85+
);
86+
}
87+
}

src/ModuleConfig.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ class ModuleConfig
108108
final public const OPTION_DEFAULT_USERS_EMAIL_ATTRIBUTE_NAME = 'users_email_attribute_name';
109109
final public const OPTION_AUTH_SOURCES_TO_USERS_EMAIL_ATTRIBUTE_NAME_MAP =
110110
'auth_sources_to_users_email_attribute_name_map';
111+
final public const OPTION_ISSUER_STATE_TTL = 'issuer_state_ttl';
111112

112113
protected static array $standardScopes = [
113114
ScopesEnum::OpenId->value => [
@@ -991,4 +992,23 @@ public function getDefaultUsersEmailAttributeName(): string
991992
{
992993
return $this->config()->getOptionalString(self::OPTION_DEFAULT_USERS_EMAIL_ATTRIBUTE_NAME, 'mail');
993994
}
995+
996+
/**
997+
* Get Issuer State Duration (TTL) if set. If not set, it will fall back to Authorization Code Duration.
998+
*
999+
* @return DateInterval
1000+
* @throws \Exception
1001+
*/
1002+
public function getIssuerStateDuration(): DateInterval
1003+
{
1004+
$issuerStateDuration = $this->config()->getOptionalString(self::OPTION_ISSUER_STATE_TTL, null);
1005+
1006+
if (is_null($issuerStateDuration)) {
1007+
return $this->getAuthCodeDuration();
1008+
}
1009+
1010+
return new DateInterval(
1011+
$this->config()->getString(self::OPTION_ISSUER_STATE_TTL),
1012+
);
1013+
}
9941014
}

0 commit comments

Comments
 (0)