|
2 | 2 |
|
3 | 3 | use Jose\Component\Core\AlgorithmManager; |
4 | 4 | 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; |
5 | 8 | use Jose\Component\KeyManagement\JWKFactory; |
6 | 9 | use Jose\Component\Signature\Algorithm\EdDSA; |
7 | 10 | use Jose\Component\Signature\Algorithm\ES256; |
@@ -1630,6 +1633,164 @@ public function testRequestUserInfoUnsignedUnencrypted() |
1630 | 1633 | $this->assertEquals($email, $userData->email); |
1631 | 1634 | } |
1632 | 1635 |
|
| 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 | + |
1633 | 1794 | public function testRequestUserInfoSignedUnencrypted() |
1634 | 1795 | { |
1635 | 1796 | // Create a new RSA key pair for signing the ID token |
@@ -1743,4 +1904,168 @@ public function testRequestUserInfoSignedUnencrypted() |
1743 | 1904 | $this->assertEquals($lastName, $userData->family_name); |
1744 | 1905 | $this->assertEquals($email, $userData->email); |
1745 | 1906 | } |
| 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 | + |
1746 | 2071 | } |
0 commit comments