Skip to content

Commit 38e98f0

Browse files
committed
Add tests for RequestUserInfo with JWE
1 parent 40cf00f commit 38e98f0

File tree

1 file changed

+325
-0
lines changed

1 file changed

+325
-0
lines changed

tests/OpenIDConnectClientTest.php

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
use Jose\Component\Core\AlgorithmManager;
44
use Jose\Component\Core\JWK;
5+
use Jose\Component\Encryption\Algorithm\ContentEncryption\A128CBCHS256;
6+
use Jose\Component\Encryption\Algorithm\KeyEncryption\RSAOAEP256;
7+
use Jose\Component\Encryption\JWEBuilder;
58
use Jose\Component\KeyManagement\JWKFactory;
69
use Jose\Component\Signature\Algorithm\EdDSA;
710
use Jose\Component\Signature\Algorithm\ES256;
@@ -1630,6 +1633,164 @@ public function testRequestUserInfoUnsignedUnencrypted()
16301633
$this->assertEquals($email, $userData->email);
16311634
}
16321635

1636+
public function testRequestUserInfoUnsignedEncrypted()
1637+
{
1638+
// Create a new RSA key pair for signing the ID token
1639+
$private_key = JWKFactory::createRSAKey(
1640+
4096,
1641+
[
1642+
'alg' => 'RS256',
1643+
'use' => 'sig'
1644+
]
1645+
);
1646+
$public_key = $private_key->toPublic();
1647+
1648+
// Create a new RSA key pair for encrypting the user info response
1649+
$encryption_key = JWKFactory::createRSAKey(
1650+
4096,
1651+
[
1652+
'alg' => 'RSA-OAEP-256',
1653+
'use' => 'enc'
1654+
]
1655+
);
1656+
1657+
1658+
// Generate random values for the ID token
1659+
$kid = bin2hex(random_bytes(6));
1660+
$code = bin2hex(random_bytes(6));
1661+
$nonce = bin2hex(random_bytes(6));
1662+
$state = bin2hex(random_bytes(6));
1663+
$firstName = $this->faker->firstName();
1664+
$lastName = $this->faker->lastName();
1665+
$email = $this->faker->email();
1666+
$sub = $this->faker->uuid();
1667+
$sid = $this->faker->uuid();
1668+
1669+
$accessToken = 'fake-access-token';
1670+
1671+
// Create claims for the ID token
1672+
$idTokenClaims = [
1673+
'exp' => time() + 60,
1674+
'iat' => time(),
1675+
'iss' => 'https://example.org',
1676+
'aud' => 'fake-client-id',
1677+
'sub' => $sub,
1678+
'sid' => $sid,
1679+
'nonce' => $nonce
1680+
];
1681+
1682+
$userInfoClaims = [
1683+
'iss' => 'https://example.org',
1684+
'aud' => 'fake-client-id',
1685+
'sub' => $sub,
1686+
'given_name' => $firstName,
1687+
'family_name' => $lastName,
1688+
'email' => $email,
1689+
];
1690+
1691+
// Create id token
1692+
$idToken = $this->signClaims($idTokenClaims, $private_key, 'RS256', ['kid' => $kid]);
1693+
1694+
// List of JWKs to be returned by the JWKS endpoint
1695+
$jwks = [[
1696+
'kid' => $kid,
1697+
...$public_key->jsonSerialize()
1698+
]];
1699+
1700+
$keyEncryptionAlgorithmManager = new AlgorithmManager([
1701+
new RSAOAEP256(),
1702+
]);
1703+
$contentEncryptionAlgorithmManager = new AlgorithmManager([
1704+
new A128CBCHS256(),
1705+
]);
1706+
1707+
$jweBuilder = new JWEBuilder(
1708+
$keyEncryptionAlgorithmManager,
1709+
$contentEncryptionAlgorithmManager,
1710+
);
1711+
1712+
$jwe = $jweBuilder
1713+
->create()
1714+
->withPayload(json_encode($userInfoClaims))
1715+
->withSharedProtectedHeader([
1716+
'alg' => 'RSA-OAEP-256',
1717+
'enc' => 'A128CBC-HS256'
1718+
])
1719+
->addRecipient($encryption_key->toPublic())
1720+
->build();
1721+
1722+
$serializer = new \Jose\Component\Encryption\Serializer\CompactSerializer();
1723+
1724+
$userInfoResponse = $serializer->serialize($jwe, 0);
1725+
1726+
// Mock the OpenIDConnectClient, only mocking the fetchURL method
1727+
$client = $this->getMockBuilder(OpenIDConnectClient::class)
1728+
->setConstructorArgs([
1729+
'https://example.org',
1730+
'fake-client-id',
1731+
'fake-client-secret',
1732+
])
1733+
->onlyMethods(['fetchURL', 'handleJweResponse'])
1734+
->getMock();
1735+
1736+
$client->expects($this->any())
1737+
->method('fetchURL')
1738+
->with($this->anything())
1739+
->will($this->returnCallback(function (string$url, ?string $post_body = null, array $headers = []) use ($userInfoResponse, $accessToken, $jwks, $client) {
1740+
switch ($url) {
1741+
case 'https://example.org/.well-known/openid-configuration':
1742+
return new Response(200, 'application/json', json_encode([
1743+
'issuer' => 'https://example.org/',
1744+
'authorization_endpoint' => 'https://example.org/authorize',
1745+
'token_endpoint' => 'https://example.org/token',
1746+
'userinfo_endpoint' => 'https://example.org/userinfo',
1747+
'jwks_uri' => 'https://example.org/jwks',
1748+
'response_types_supported' => ['code', 'id_token'],
1749+
'subject_types_supported' => ['public'],
1750+
'id_token_signing_alg_values_supported' => ['RS256'],
1751+
]));
1752+
case 'https://example.org/jwks':
1753+
return new Response(200, 'application/json', json_encode([
1754+
'keys' => $jwks
1755+
]));
1756+
case 'https://example.org/userinfo':
1757+
$this->assertEquals('Authorization: Bearer '.$accessToken, $headers[0]);
1758+
return new Response(200, 'application/jwt', $userInfoResponse);
1759+
default:
1760+
throw new Exception("Unexpected request: $url");
1761+
}
1762+
}));
1763+
1764+
$client->expects($this->any())
1765+
->method('handleJweResponse')
1766+
->with($this->anything())
1767+
->will(
1768+
$this->returnCallback(function (string $jwe) use ($userInfoClaims, $userInfoResponse) {
1769+
$this->assertEquals($userInfoResponse, $jwe);
1770+
return json_encode($userInfoClaims);
1771+
})
1772+
);
1773+
1774+
// Simulate the state and nonce have been set in the session
1775+
$_SESSION['openid_connect_state'] = $state;
1776+
$_SESSION['openid_connect_nonce'] = $nonce;
1777+
1778+
// Simulate incoming request with code and state
1779+
$_REQUEST['code'] = $code;
1780+
$_REQUEST['state'] = $state;
1781+
1782+
$client->setAccessToken($accessToken);
1783+
$client->setIdToken($idToken);
1784+
1785+
// Get user info
1786+
$userData = $client->requestUserInfo();
1787+
1788+
// Verify call claims are correctly retrieved
1789+
$this->assertEquals($firstName, $userData->given_name);
1790+
$this->assertEquals($lastName, $userData->family_name);
1791+
$this->assertEquals($email, $userData->email);
1792+
}
1793+
16331794
public function testRequestUserInfoSignedUnencrypted()
16341795
{
16351796
// Create a new RSA key pair for signing the ID token
@@ -1743,4 +1904,168 @@ public function testRequestUserInfoSignedUnencrypted()
17431904
$this->assertEquals($lastName, $userData->family_name);
17441905
$this->assertEquals($email, $userData->email);
17451906
}
1907+
1908+
public function testRequestUserInfoSignedEncrypted()
1909+
{
1910+
// Create a new RSA key pair for signing the ID token
1911+
$private_key = JWKFactory::createRSAKey(
1912+
4096,
1913+
[
1914+
'alg' => 'RS256',
1915+
'use' => 'sig'
1916+
]
1917+
);
1918+
$public_key = $private_key->toPublic();
1919+
1920+
// Create a new RSA key pair for encrypting the user info response
1921+
$encryption_key = JWKFactory::createRSAKey(
1922+
4096,
1923+
[
1924+
'alg' => 'RSA-OAEP-256',
1925+
'use' => 'enc'
1926+
]
1927+
);
1928+
1929+
1930+
// Generate random values for the ID token
1931+
$kid = bin2hex(random_bytes(6));
1932+
$code = bin2hex(random_bytes(6));
1933+
$nonce = bin2hex(random_bytes(6));
1934+
$state = bin2hex(random_bytes(6));
1935+
$firstName = $this->faker->firstName();
1936+
$lastName = $this->faker->lastName();
1937+
$email = $this->faker->email();
1938+
$sub = $this->faker->uuid();
1939+
$sid = $this->faker->uuid();
1940+
1941+
$accessToken = 'fake-access-token';
1942+
1943+
// Create claims for the ID token
1944+
$idTokenClaims = [
1945+
'exp' => time() + 60,
1946+
'iat' => time(),
1947+
'iss' => 'https://example.org',
1948+
'aud' => 'fake-client-id',
1949+
'sub' => $sub,
1950+
'sid' => $sid,
1951+
'nonce' => $nonce
1952+
];
1953+
1954+
$userInfoClaims = [
1955+
'iss' => 'https://example.org',
1956+
'aud' => 'fake-client-id',
1957+
'sub' => $sub,
1958+
'given_name' => $firstName,
1959+
'family_name' => $lastName,
1960+
'email' => $email,
1961+
];
1962+
1963+
// Create id token
1964+
$idToken = $this->signClaims($idTokenClaims, $private_key, 'RS256', ['kid' => $kid]);
1965+
1966+
// List of JWKs to be returned by the JWKS endpoint
1967+
$jwks = [[
1968+
'kid' => $kid,
1969+
...$public_key->jsonSerialize()
1970+
]];
1971+
1972+
1973+
$keyEncryptionAlgorithmManager = new AlgorithmManager([
1974+
new RSAOAEP256(),
1975+
]);
1976+
$contentEncryptionAlgorithmManager = new AlgorithmManager([
1977+
new A128CBCHS256(),
1978+
]);
1979+
1980+
$jweBuilder = new JWEBuilder(
1981+
$keyEncryptionAlgorithmManager,
1982+
$contentEncryptionAlgorithmManager,
1983+
);
1984+
1985+
$jws = $this->signClaims($userInfoClaims, $private_key, 'RS256', ['kid' => $kid]);
1986+
1987+
$jwe = $jweBuilder
1988+
->create()
1989+
->withPayload($jws)
1990+
->withSharedProtectedHeader([
1991+
'alg' => 'RSA-OAEP-256',
1992+
'enc' => 'A128CBC-HS256',
1993+
'cty' => 'JWT',
1994+
])
1995+
->addRecipient($encryption_key->toPublic())
1996+
->build();
1997+
1998+
$serializer = new \Jose\Component\Encryption\Serializer\CompactSerializer();
1999+
2000+
$userInfoResponse = $serializer->serialize($jwe, 0);
2001+
2002+
// Mock the OpenIDConnectClient, only mocking the fetchURL method
2003+
$client = $this->getMockBuilder(OpenIDConnectClient::class)
2004+
->setConstructorArgs([
2005+
'https://example.org',
2006+
'fake-client-id',
2007+
'fake-client-secret',
2008+
])
2009+
->onlyMethods(['fetchURL', 'handleJweResponse'])
2010+
->getMock();
2011+
2012+
$client->expects($this->any())
2013+
->method('fetchURL')
2014+
->with($this->anything())
2015+
->will($this->returnCallback(function (string$url, ?string $post_body = null, array $headers = []) use ($userInfoResponse, $accessToken, $jwks, $client) {
2016+
switch ($url) {
2017+
case 'https://example.org/.well-known/openid-configuration':
2018+
return new Response(200, 'application/json', json_encode([
2019+
'issuer' => 'https://example.org/',
2020+
'authorization_endpoint' => 'https://example.org/authorize',
2021+
'token_endpoint' => 'https://example.org/token',
2022+
'userinfo_endpoint' => 'https://example.org/userinfo',
2023+
'jwks_uri' => 'https://example.org/jwks',
2024+
'response_types_supported' => ['code', 'id_token'],
2025+
'subject_types_supported' => ['public'],
2026+
'id_token_signing_alg_values_supported' => ['RS256'],
2027+
]));
2028+
case 'https://example.org/jwks':
2029+
return new Response(200, 'application/json', json_encode([
2030+
'keys' => $jwks
2031+
]));
2032+
case 'https://example.org/userinfo':
2033+
$this->assertEquals('Authorization: Bearer '.$accessToken, $headers[0]);
2034+
return new Response(200, 'application/jwt', $userInfoResponse);
2035+
default:
2036+
throw new Exception("Unexpected request: $url");
2037+
}
2038+
}));
2039+
2040+
$client->expects($this->any())
2041+
->method('handleJweResponse')
2042+
->with($this->anything())
2043+
->will(
2044+
$this->returnCallback(function (string $jwe) use ($jws, $userInfoResponse) {
2045+
$this->assertEquals($userInfoResponse, $jwe);
2046+
return $jws;
2047+
})
2048+
);
2049+
2050+
// Simulate the state and nonce have been set in the session
2051+
$_SESSION['openid_connect_state'] = $state;
2052+
$_SESSION['openid_connect_nonce'] = $nonce;
2053+
2054+
// Simulate incoming request with code and state
2055+
$_REQUEST['code'] = $code;
2056+
$_REQUEST['state'] = $state;
2057+
2058+
$client->setAccessToken($accessToken);
2059+
$client->setIdToken($idToken);
2060+
2061+
// Get user info
2062+
$userData = $client->requestUserInfo();
2063+
2064+
// Verify call claims are correctly retrieved
2065+
$this->assertEquals($firstName, $userData->given_name);
2066+
$this->assertEquals($lastName, $userData->family_name);
2067+
$this->assertEquals($email, $userData->email);
2068+
}
2069+
2070+
17462071
}

0 commit comments

Comments
 (0)