Skip to content

Commit e79db9c

Browse files
committed
Add VCI Nonce TTL config
1 parent 88b706a commit e79db9c

5 files changed

Lines changed: 84 additions & 11 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"psr/container": "^2.0",
3232
"psr/log": "^3",
3333
"simplesamlphp/composer-module-installer": "^1.3",
34-
"simplesamlphp/openid": "~v0.3.4",
34+
"simplesamlphp/openid": "~v0.3.5",
3535
"spomky-labs/base64url": "^2.0",
3636
"symfony/expression-language": "^7.4",
3737
"symfony/psr-http-message-bridge": "^7.4",

config/module_oidc.php.dist

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1261,6 +1261,14 @@ $config = [
12611261
*/
12621262
ModuleConfig::OPTION_VCI_ISSUER_STATE_TTL => 'PT10M', // 10 minutes
12631263

1264+
/**
1265+
* (optional) Nonce TTL (validity duration) used for VCI proof-of-possession
1266+
* nonces, with the given example. If not set, defaults to 5 minutes. For
1267+
* duration format info, check
1268+
* https://www.php.net/manual/en/dateinterval.construct.php
1269+
*/
1270+
ModuleConfig::OPTION_VCI_NONCE_TTL => 'PT5M', // 5 minutes
1271+
12641272
/**
12651273
* Map of authentication sources and user's email attribute names. This
12661274
* enables you to define a specific attribute name which contains the

src/ModuleConfig.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ class ModuleConfig
114114
final public const string OPTION_AUTH_SOURCES_TO_USERS_EMAIL_ATTRIBUTE_NAME_MAP =
115115
'auth_sources_to_users_email_attribute_name_map';
116116
final public const string OPTION_VCI_ISSUER_STATE_TTL = 'vci_issuer_state_ttl';
117+
final public const string OPTION_VCI_NONCE_TTL = 'vci_nonce_ttl';
117118
final public const string OPTION_VCI_ALLOW_NON_REGISTERED_CLIENTS = 'vci_allow_non_registered_clients';
118119
final public const string OPTION_VCI_ALLOWED_REDIRECT_URI_PREFIXES_FOR_NON_REGISTERED_CLIENTS =
119120
'vci_allowed_redirect_uri_prefixes_for_non_registered_clients';
@@ -1130,6 +1131,24 @@ public function getVciIssuerStateDuration(): DateInterval
11301131
);
11311132
}
11321133

1134+
/**
1135+
* Get Nonce TTL (validity duration) used for VCI proof-of-possession
1136+
* nonces. If not set, it defaults to 5 minutes.
1137+
*
1138+
* @return DateInterval
1139+
* @throws \Exception
1140+
*/
1141+
public function getVciNonceTtl(): DateInterval
1142+
{
1143+
$nonceTtl = $this->config()->getOptionalString(self::OPTION_VCI_NONCE_TTL, null);
1144+
1145+
if (is_null($nonceTtl)) {
1146+
return new DateInterval('PT5M');
1147+
}
1148+
1149+
return new DateInterval($nonceTtl);
1150+
}
1151+
11331152
public function getVciAllowNonRegisteredClients(): bool
11341153
{
11351154
return $this->config()->getOptionalBoolean(self::OPTION_VCI_ALLOW_NON_REGISTERED_CLIENTS, false);

src/Services/NonceService.php

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace SimpleSAML\Module\oidc\Services;
66

7+
use SimpleSAML\Module\oidc\Helpers;
78
use SimpleSAML\Module\oidc\ModuleConfig;
89
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
910
use SimpleSAML\OpenID\Jws;
@@ -14,6 +15,7 @@ public function __construct(
1415
protected readonly Jws $jws,
1516
protected readonly ModuleConfig $moduleConfig,
1617
protected readonly LoggerService $loggerService,
18+
protected readonly Helpers $helpers,
1719
) {
1820
}
1921

@@ -23,17 +25,17 @@ public function __construct(
2325
public function generateNonce(): string
2426
{
2527
$signatureKeyPair = $this->moduleConfig->getVciSignatureKeyPairBag()->getFirstOrFail();
26-
$currentTimestamp = $this->jws->helpers()->dateTime()->getUtc()->getTimestamp();
28+
$currentDateTime = $this->jws->helpers()->dateTime()->getUtc();
29+
$currentTimestamp = $currentDateTime->getTimestamp();
2730

28-
// Nonce is valid for 5 minutes (300 seconds)
29-
// TODO mivanci Consider making this configurable.
30-
$expiryTimestamp = $currentTimestamp + 300;
31+
// Nonce is valid for the configured TTL (defaults to 5 minutes).
32+
$expiryTimestamp = $currentDateTime->add($this->moduleConfig->getVciNonceTtl())->getTimestamp();
3133

3234
$payload = [
3335
ClaimsEnum::Iss->value => $this->moduleConfig->getIssuer(),
3436
ClaimsEnum::Iat->value => $currentTimestamp,
3537
ClaimsEnum::Exp->value => $expiryTimestamp,
36-
'nonce_val' => bin2hex(random_bytes(16)),
38+
ClaimsEnum::NonceVal->value => $this->helpers->random()->getIdentifier(16),
3739
];
3840

3941
$header = [

tests/unit/src/Services/NonceServiceTest.php

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
use PHPUnit\Framework\Attributes\CoversClass;
99
use PHPUnit\Framework\MockObject\MockObject;
1010
use PHPUnit\Framework\TestCase;
11+
use SimpleSAML\Module\oidc\Helpers as OidcHelpers;
12+
use SimpleSAML\Module\oidc\Helpers\Random as OidcRandom;
1113
use SimpleSAML\Module\oidc\ModuleConfig;
1214
use SimpleSAML\Module\oidc\Services\LoggerService;
1315
use SimpleSAML\Module\oidc\Services\NonceService;
@@ -34,6 +36,8 @@ class NonceServiceTest extends TestCase
3436
protected MockObject $signatureKeyPairMock;
3537
protected MockObject $helpersMock;
3638
protected MockObject $dateTimeHelperMock;
39+
protected MockObject $oidcHelpersMock;
40+
protected MockObject $oidcRandomMock;
3741

3842
public function setUp(): void
3943
{
@@ -44,10 +48,13 @@ public function setUp(): void
4448
$this->parsedJwsMock = $this->createMock(ParsedJws::class);
4549
$this->helpersMock = $this->createMock(Helpers::class);
4650
$this->dateTimeHelperMock = $this->createMock(DateTime::class);
51+
$this->oidcHelpersMock = $this->createMock(OidcHelpers::class);
52+
$this->oidcRandomMock = $this->createMock(OidcRandom::class);
4753

4854
$this->jwsMock->method('parsedJwsFactory')->willReturn($this->parsedJwsFactoryMock);
4955
$this->jwsMock->method('helpers')->willReturn($this->helpersMock);
5056
$this->helpersMock->method('dateTime')->willReturn($this->dateTimeHelperMock);
57+
$this->oidcHelpersMock->method('random')->willReturn($this->oidcRandomMock);
5158

5259
$this->signatureKeyPairMock = $this->createMock(SignatureKeyPair::class);
5360
$this->signatureKeyPairBagMock = $this->createMock(SignatureKeyPairBag::class);
@@ -57,8 +64,10 @@ public function setUp(): void
5764

5865
public function testGenerateNonce(): void
5966
{
60-
$this->dateTimeHelperMock->method('getUtc')->willReturn(new \DateTimeImmutable('2024-01-01 00:00:00'));
67+
$currentDateTime = new \DateTimeImmutable('2024-01-01 00:00:00');
68+
$this->dateTimeHelperMock->method('getUtc')->willReturn($currentDateTime);
6169
$this->moduleConfigMock->method('getIssuer')->willReturn('https://issuer.example.com');
70+
$this->moduleConfigMock->method('getVciNonceTtl')->willReturn(new \DateInterval('PT5M'));
6271

6372
$privateKeyMock = $this->createMock(JwkDecorator::class);
6473
$keyPairMock = $this->createMock(KeyPair::class);
@@ -67,13 +76,33 @@ public function testGenerateNonce(): void
6776
$this->signatureKeyPairMock->method('getKeyPair')->willReturn($keyPairMock);
6877
$this->signatureKeyPairMock->method('getSignatureAlgorithm')->willReturn(SignatureAlgorithmEnum::ES256);
6978

79+
$this->oidcRandomMock->expects($this->once())
80+
->method('getIdentifier')
81+
->with(16)
82+
->willReturn('mocked_random_nonce');
83+
7084
$this->parsedJwsFactoryMock->expects($this->once())
7185
->method('fromData')
86+
->with(
87+
$this->anything(),
88+
$this->anything(),
89+
$this->callback(function (array $payload) use ($currentDateTime): bool {
90+
return $payload['iat'] === $currentDateTime->getTimestamp()
91+
&& $payload['exp'] === $currentDateTime->getTimestamp() + 300
92+
&& $payload['nonce_val'] === 'mocked_random_nonce';
93+
}),
94+
$this->anything(),
95+
)
7296
->willReturn($this->parsedJwsMock);
7397

7498
$this->parsedJwsMock->method('getToken')->willReturn('mocked_token');
7599

76-
$sut = new NonceService($this->jwsMock, $this->moduleConfigMock, $this->loggerServiceMock);
100+
$sut = new NonceService(
101+
$this->jwsMock,
102+
$this->moduleConfigMock,
103+
$this->loggerServiceMock,
104+
$this->oidcHelpersMock,
105+
);
77106
$nonce = $sut->generateNonce();
78107

79108
$this->assertEquals('mocked_token', $nonce);
@@ -98,7 +127,12 @@ public function testValidateNonceSuccess(): void
98127
$this->parsedJwsMock->method('getExpirationTime')
99128
->willReturn((new \DateTimeImmutable('2024-01-01 00:00:00'))->getTimestamp() + 100);
100129

101-
$sut = new NonceService($this->jwsMock, $this->moduleConfigMock, $this->loggerServiceMock);
130+
$sut = new NonceService(
131+
$this->jwsMock,
132+
$this->moduleConfigMock,
133+
$this->loggerServiceMock,
134+
$this->oidcHelpersMock,
135+
);
102136
$this->assertTrue($sut->validateNonce('valid_token'));
103137
}
104138

@@ -119,7 +153,12 @@ public function testValidateNonceInvalidIssuer(): void
119153
$this->parsedJwsMock->method('getIssuer')->willReturn('https://other.example.com');
120154
$this->moduleConfigMock->method('getIssuer')->willReturn('https://issuer.example.com');
121155

122-
$sut = new NonceService($this->jwsMock, $this->moduleConfigMock, $this->loggerServiceMock);
156+
$sut = new NonceService(
157+
$this->jwsMock,
158+
$this->moduleConfigMock,
159+
$this->loggerServiceMock,
160+
$this->oidcHelpersMock,
161+
);
123162
$this->assertFalse($sut->validateNonce('invalid_issuer_token'));
124163
}
125164

@@ -142,7 +181,12 @@ public function testValidateNonceExpired(): void
142181
$this->parsedJwsMock->method('getExpirationTime')
143182
->willReturn((new \DateTimeImmutable('2024-01-01 00:00:00'))->getTimestamp() - 10);
144183

145-
$sut = new NonceService($this->jwsMock, $this->moduleConfigMock, $this->loggerServiceMock);
184+
$sut = new NonceService(
185+
$this->jwsMock,
186+
$this->moduleConfigMock,
187+
$this->loggerServiceMock,
188+
$this->oidcHelpersMock,
189+
);
146190
$this->assertFalse($sut->validateNonce('expired_token'));
147191
}
148192
}

0 commit comments

Comments
 (0)