Skip to content

Commit 74883f5

Browse files
committed
WIP
1 parent 53802b2 commit 74883f5

12 files changed

Lines changed: 1823 additions & 5 deletions

docs/1-openid.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,5 @@
22

33
1. [Installation](2-installation.md)
44
2. [OpenID Federation Tools](3-federation.md)
5-
2.1 [Federation Discovery](3.1-federation-discovery.md)
5+
2.1 [Federation Discovery and Entity Collection](3.1-federation-discovery.md)
66
3. [OpenID for Verifiable Credential Issuance (OpenID4VCI) Tools](4-vci.md)
7-
4. [Federation Discovery and Entity Collection](5-federation-discovery.md)

src/Helpers/Arr.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,7 @@ public function addNestedValue(array &$array, mixed $value, int|string ...$keys)
135135
*/
136136
public function getNestedValue(array $array, int|string ...$keys): mixed
137137
{
138-
if (count($keys) > 99) {
139-
throw new OpenIdException('Refusing to recurse to given depth.');
140-
}
138+
$this->validateMaxDepth(count($keys));
141139

142140
if (count($keys) < 1) {
143141
return null;
@@ -162,6 +160,10 @@ public function getNestedValue(array $array, int|string ...$keys): mixed
162160
*/
163161
public function isAssociative(array $array): bool
164162
{
163+
if ($array === []) {
164+
return false;
165+
}
166+
165167
// Has at least one string key or non-sequential numeric keys
166168
return array_keys($array) !== range(0, count($array) - 1);
167169
}

tests/src/Core/LogoutTokenTest.php

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Test\OpenID\Core;
6+
7+
use Jose\Component\Signature\JWS;
8+
use PHPUnit\Framework\Attributes\CoversClass;
9+
use PHPUnit\Framework\MockObject\MockObject;
10+
use PHPUnit\Framework\TestCase;
11+
use SimpleSAML\OpenID\Core\LogoutToken;
12+
use SimpleSAML\OpenID\Decorators\DateIntervalDecorator;
13+
use SimpleSAML\OpenID\Exceptions\JwsException;
14+
use SimpleSAML\OpenID\Factories\ClaimFactory;
15+
use SimpleSAML\OpenID\Helpers;
16+
use SimpleSAML\OpenID\Jwks\Factories\JwksDecoratorFactory;
17+
use SimpleSAML\OpenID\Jws\JwsDecorator;
18+
use SimpleSAML\OpenID\Jws\JwsVerifierDecorator;
19+
use SimpleSAML\OpenID\Serializers\JwsSerializerManagerDecorator;
20+
21+
#[CoversClass(LogoutToken::class)]
22+
final class LogoutTokenTest extends TestCase
23+
{
24+
protected MockObject $jwsDecoratorMock;
25+
26+
protected \PHPUnit\Framework\MockObject\Stub $jwsVerifierDecoratorMock;
27+
28+
protected \PHPUnit\Framework\MockObject\Stub $jwksDecoratorFactoryMock;
29+
30+
protected \PHPUnit\Framework\MockObject\Stub $jwsSerializerManagerDecoratorMock;
31+
32+
protected \PHPUnit\Framework\MockObject\Stub $dateIntervalDecoratorMock;
33+
34+
protected MockObject $helpersMock;
35+
36+
protected MockObject $jsonHelperMock;
37+
38+
protected \PHPUnit\Framework\MockObject\Stub $claimFactoryMock;
39+
40+
protected array $validPayload;
41+
42+
43+
protected function setUp(): void
44+
{
45+
$jwsMock = $this->createMock(JWS::class);
46+
$jwsMock->method('getPayload')
47+
->willReturn('json-payload-string');
48+
49+
$this->jwsDecoratorMock = $this->createMock(JwsDecorator::class);
50+
$this->jwsDecoratorMock->method('jws')->willReturn($jwsMock);
51+
52+
$this->jwsVerifierDecoratorMock = $this->createStub(JwsVerifierDecorator::class);
53+
$this->jwksDecoratorFactoryMock = $this->createStub(JwksDecoratorFactory::class);
54+
$this->jwsSerializerManagerDecoratorMock = $this->createStub(JwsSerializerManagerDecorator::class);
55+
$this->dateIntervalDecoratorMock = $this->createStub(DateIntervalDecorator::class);
56+
57+
$this->helpersMock = $this->createMock(Helpers::class);
58+
$this->jsonHelperMock = $this->createMock(Helpers\Json::class);
59+
$this->helpersMock->method('json')->willReturn($this->jsonHelperMock);
60+
$typeHelperMock = $this->createMock(Helpers\Type::class);
61+
$this->helpersMock->method('type')->willReturn($typeHelperMock);
62+
63+
$typeHelperMock->method('ensureNonEmptyString')->willReturnArgument(0);
64+
$typeHelperMock->method('ensureInt')->willReturnArgument(0);
65+
$typeHelperMock->method('enforceUri')->willReturnArgument(0);
66+
$typeHelperMock->method('ensureArrayWithValuesAsStrings')->willReturnArgument(0);
67+
68+
$this->claimFactoryMock = $this->createStub(ClaimFactory::class);
69+
70+
$this->validPayload = [
71+
'iss' => 'https://server.example.com',
72+
'sub' => '24400320',
73+
'aud' => 's6BhdRkqt3',
74+
'iat' => time(),
75+
'exp' => time() + 3600,
76+
'jti' => 'bWJq',
77+
'events' => [
78+
'http://schemas.openid.net/event/backchannel-logout' => (object) [],
79+
],
80+
];
81+
}
82+
83+
84+
protected function sut(
85+
?JwsDecorator $jwsDecorator = null,
86+
?JwsVerifierDecorator $jwsVerifierDecorator = null,
87+
?JwksDecoratorFactory $jwksDecoratorFactory = null,
88+
?JwsSerializerManagerDecorator $jwsSerializerManagerDecorator = null,
89+
?DateIntervalDecorator $dateIntervalDecorator = null,
90+
?Helpers $helpers = null,
91+
?ClaimFactory $claimFactory = null,
92+
): LogoutToken {
93+
$jwsDecorator ??= $this->jwsDecoratorMock;
94+
$jwsVerifierDecorator ??= $this->jwsVerifierDecoratorMock;
95+
$jwksDecoratorFactory ??= $this->jwksDecoratorFactoryMock;
96+
$jwsSerializerManagerDecorator ??= $this->jwsSerializerManagerDecoratorMock;
97+
$dateIntervalDecorator ??= $this->dateIntervalDecoratorMock;
98+
$helpers ??= $this->helpersMock;
99+
$claimFactory ??= $this->claimFactoryMock;
100+
101+
return new LogoutToken(
102+
$jwsDecorator,
103+
$jwsVerifierDecorator,
104+
$jwksDecoratorFactory,
105+
$jwsSerializerManagerDecorator,
106+
$dateIntervalDecorator,
107+
$helpers,
108+
$claimFactory,
109+
);
110+
}
111+
112+
113+
public function testCanCreateInstance(): void
114+
{
115+
$this->jsonHelperMock->method('decode')->willReturn($this->validPayload);
116+
$this->assertInstanceOf(LogoutToken::class, $this->sut());
117+
}
118+
119+
120+
public function testCanGetRequiredClaims(): void
121+
{
122+
$this->jsonHelperMock->method('decode')->willReturn($this->validPayload);
123+
$sut = $this->sut();
124+
125+
$this->assertSame($this->validPayload['iss'], $sut->getIssuer());
126+
$this->assertSame([$this->validPayload['aud']], $sut->getAudience());
127+
$this->assertSame($this->validPayload['iat'], $sut->getIssuedAt());
128+
$this->assertSame($this->validPayload['exp'], $sut->getExpirationTime());
129+
$this->assertSame($this->validPayload['jti'], $sut->getJwtId());
130+
$this->assertSame($this->validPayload['events'], $sut->getEvents());
131+
}
132+
133+
134+
public function testGetIssuerThrowsWhenMissing(): void
135+
{
136+
$payload = $this->validPayload;
137+
unset($payload['iss']);
138+
$this->jsonHelperMock->method('decode')->willReturn($payload);
139+
140+
$this->expectException(JwsException::class);
141+
$this->expectExceptionMessage('No Issuer claim found.');
142+
143+
$this->sut();
144+
}
145+
146+
147+
public function testGetAudienceThrowsWhenMissing(): void
148+
{
149+
$payload = $this->validPayload;
150+
unset($payload['aud']);
151+
$this->jsonHelperMock->method('decode')->willReturn($payload);
152+
153+
$this->expectException(JwsException::class);
154+
$this->expectExceptionMessage('No Audience claim found.');
155+
156+
$this->sut();
157+
}
158+
159+
160+
public function testGetIssuedAtThrowsWhenMissing(): void
161+
{
162+
$payload = $this->validPayload;
163+
unset($payload['iat']);
164+
$this->jsonHelperMock->method('decode')->willReturn($payload);
165+
166+
$this->expectException(JwsException::class);
167+
$this->expectExceptionMessage('No Issued At claim found.');
168+
169+
$this->sut();
170+
}
171+
172+
173+
public function testGetExpirationTimeThrowsWhenMissing(): void
174+
{
175+
$payload = $this->validPayload;
176+
unset($payload['exp']);
177+
$this->jsonHelperMock->method('decode')->willReturn($payload);
178+
179+
$this->expectException(JwsException::class);
180+
$this->expectExceptionMessage('No Expiration Time claim found.');
181+
182+
$this->sut();
183+
}
184+
185+
186+
public function testGetJwtIdThrowsWhenMissing(): void
187+
{
188+
$payload = $this->validPayload;
189+
unset($payload['jti']);
190+
$this->jsonHelperMock->method('decode')->willReturn($payload);
191+
192+
$this->expectException(JwsException::class);
193+
$this->expectExceptionMessage('No JWT ID claim found.');
194+
195+
$this->sut();
196+
}
197+
198+
199+
public function testGetEventsThrowsWhenMissing(): void
200+
{
201+
$payload = $this->validPayload;
202+
unset($payload['events']);
203+
$this->jsonHelperMock->method('decode')->willReturn($payload);
204+
205+
$this->expectException(JwsException::class);
206+
$this->expectExceptionMessage('No Events claim found.');
207+
208+
$this->sut();
209+
}
210+
211+
212+
public function testGetEventsThrowsWhenMalformed(): void
213+
{
214+
$payload = $this->validPayload;
215+
$payload['events'] = ['wrong-event' => []];
216+
$this->jsonHelperMock->method('decode')->willReturn($payload);
217+
218+
$this->expectException(JwsException::class);
219+
$this->expectExceptionMessage('Malformed events claim.');
220+
221+
$this->sut();
222+
}
223+
224+
225+
public function testCanGetSessionId(): void
226+
{
227+
$payload = $this->validPayload;
228+
$payload['sid'] = 'session-id';
229+
$this->jsonHelperMock->method('decode')->willReturn($payload);
230+
231+
$this->assertSame('session-id', $this->sut()->getSessionId());
232+
}
233+
234+
235+
public function testGetNonceThrowsWhenPresent(): void
236+
{
237+
$payload = $this->validPayload;
238+
$payload['nonce'] = 'some-nonce';
239+
$this->jsonHelperMock->method('decode')->willReturn($payload);
240+
241+
$this->expectException(JwsException::class);
242+
$this->expectExceptionMessage('Nonce claim is forbidden in Logout Token.');
243+
244+
$this->sut();
245+
}
246+
247+
248+
public function testThrowsWhenBothSubAndSidAreMissing(): void
249+
{
250+
$payload = $this->validPayload;
251+
unset($payload['sub']);
252+
unset($payload['sid']);
253+
$this->jsonHelperMock->method('decode')->willReturn($payload);
254+
255+
$this->expectException(JwsException::class);
256+
$this->expectExceptionMessage('Missing Subject and Session ID claim in Logout Token.');
257+
258+
$this->sut();
259+
}
260+
261+
262+
public function testDoesNotThrowWhenSubIsMissingButSidIsPresent(): void
263+
{
264+
$payload = $this->validPayload;
265+
unset($payload['sub']);
266+
$payload['sid'] = 'session-id';
267+
$this->jsonHelperMock->method('decode')->willReturn($payload);
268+
269+
$this->assertInstanceOf(LogoutToken::class, $this->sut());
270+
}
271+
272+
273+
public function testDoesNotThrowWhenSidIsMissingButSubIsPresent(): void
274+
{
275+
$payload = $this->validPayload;
276+
unset($payload['sid']);
277+
// sub is already in validPayload
278+
$this->jsonHelperMock->method('decode')->willReturn($payload);
279+
280+
$this->assertInstanceOf(LogoutToken::class, $this->sut());
281+
}
282+
}

0 commit comments

Comments
 (0)