From a7648fa5277be520fc1d21a659b9847bf936e3ab Mon Sep 17 00:00:00 2001 From: Kevin Gao Date: Thu, 6 Jul 2023 10:23:18 -0700 Subject: [PATCH 01/21] RELEASE --- .github/workflows/main.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 92ed7c3..78459e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,8 +32,7 @@ jobs: minor-wording: "MINOR" env: GITHUB_TOKEN: ${{ steps.get_token.outputs.token }} - - name: cat package.json - run: cat ./package.json + - name: Output Step env: NEW_TAG: ${{ steps.version-bump.outputs.newTag }} From 97374ad2819c577ce26f33c19b3c57c0ae60eb75 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Thu, 12 Dec 2024 21:29:26 -0800 Subject: [PATCH 02/21] fixed validateToken function --- src/SDK/Token/Verifier.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/SDK/Token/Verifier.php b/src/SDK/Token/Verifier.php index addc1e8..deaddf8 100644 --- a/src/SDK/Token/Verifier.php +++ b/src/SDK/Token/Verifier.php @@ -71,7 +71,6 @@ public function verify($sessionToken, ?string $audience = null) public function refreshSession(string $refreshToken): array { $this->validateRefreshTokenNotNil($refreshToken); - $this->validateToken($refreshToken); $uri = EndpointsV1::$REFRESH_TOKEN_PATH; $response = $this->doPost($uri, [], $refreshToken); return $this->generateJwtResponse($response, $refreshToken); @@ -92,7 +91,7 @@ public function verifyAndRefreshSession(string $sessionToken, string $refreshTok } try { - $this->validateToken($sessionToken); + $this->verify($sessionToken); return $this->generateJwtResponse($sessionToken, $refreshToken); } catch (AuthException $e) { return $this->refreshSession($refreshToken); From 4d46a32af8acd306fd9ea61e774407ba73711fb2 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:49:46 -0800 Subject: [PATCH 03/21] added error handling --- .github/workflows/phpunit.yml | 24 +++++++ phpunit.xml | 8 +++ sample/dashboard.php | 5 ++ sample/login.php | 2 +- src/SDK/DescopeSDK.php | 97 ++++++++++++++++++-------- src/SDK/Management/Role.php | 95 +++++++++++++++++++++++++ src/SDK/Token/Verifier.php | 126 ++-------------------------------- src/tests/DescopeSDKTest.php | 49 ++++++------- 8 files changed, 227 insertions(+), 179 deletions(-) create mode 100644 .github/workflows/phpunit.yml create mode 100644 phpunit.xml create mode 100644 src/SDK/Management/Role.php diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..445a05b --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,24 @@ +name: PHP Unit Tests + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + + - name: Install dependencies + run: composer install + + - name: Run tests + run: vendor/bin/phpunit --configuration phpunit.xml \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..8e6191a --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,8 @@ + + + + + tests + + + \ No newline at end of file diff --git a/sample/dashboard.php b/sample/dashboard.php index 527189e..a8e176e 100644 --- a/sample/dashboard.php +++ b/sample/dashboard.php @@ -1,5 +1,6 @@ $_ENV['DESCOPE_PROJECT_ID'] +]); ?> diff --git a/sample/login.php b/sample/login.php index 8e37477..7c0d070 100644 --- a/sample/login.php +++ b/sample/login.php @@ -76,7 +76,7 @@ function sendFormData(sessionToken, userDetails) { sdk.logout(); console.log("No valid refresh token. Displaying login form."); const container = document.getElementById("container") - container.innerHTML = ''; + container.innerHTML = ''; const wcElement = document.getElementsByTagName('descope-wc')[0]; const onSuccess = async (e) => { diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 67ac5a2..2313818 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -12,6 +12,8 @@ use Descope\SDK\Auth\Management\User; use Descope\SDK\Auth\Management\Audit; use Descope\SDK\EndpointsV1; +use Descope\SDK\EndpointsV2; + use Descope\SDK\Management\MgmtV1; class DescopeSDK @@ -45,6 +47,8 @@ public function __construct(array $config) MgmtV1::setBaseUrl($config['projectId']); } + $this->verifier = new Verifier($this->config, $this->api); + $this->password = new Password($this->api); $this->sso = new SSO($this->api); } @@ -58,14 +62,14 @@ public function __construct(array $config) */ public function verify($sessionToken = null) { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; if (!$sessionToken) { throw new \InvalidArgumentException('Session token is required.'); } - $verifier = new Verifier($this->config); - return $verifier->verify($sessionToken); + $verifier = new Verifier($this->config, $this->api); + return $this->verifier->verify($sessionToken); } /** @@ -79,12 +83,22 @@ public function refreshSession($refreshToken = null) { $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; - if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + if (empty($refreshToken)) { + throw new AuthException('Refresh token cannot be null or empty.'); } - $verifier = new Verifier($this->config); - return $verifier->refreshSession($refreshToken); + try { + return $this->api->doPost( + EndpointsV1::$REFRESH_TOKEN_PATH, + [], + false, + $refreshToken + ); + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** @@ -100,12 +114,16 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; - if (!$sessionToken || !$refreshToken) { - throw new \InvalidArgumentException('Session token and refresh token are required.'); + if (empty($sessionToken) || empty($refreshToken)) { + throw new AuthException(400, 'Session or refresh token cannot be null or empty.'); + } + + try { + $this->verify($sessionToken); + return $this->refreshSession($refreshToken); + } catch (AuthException $e) { + return $this->refreshSession($refreshToken); } - - $verifier = new Verifier($this->config); - return $verifier->verifyAndRefreshSession($sessionToken, $refreshToken); } /** @@ -142,11 +160,18 @@ public function getUserDetails(string $refreshToken = null) throw new \InvalidArgumentException('Refresh token is required.'); } - return $this->api->doGet( - EndpointsV1::$ME_PATH, - false, - $refreshToken - ); + try { + $this->api->doGet( + EndpointsV1::$ME_PATH, + false, + $refreshToken + ); + return; + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** @@ -164,12 +189,19 @@ public function logout(string $refreshToken = null): void throw new \InvalidArgumentException('Refresh token is required.'); } - $this->api->doPost( - EndpointsV1::$LOGOUT_PATH, - [], - false, - $refreshToken - ); + try { + $this->api->doPost( + EndpointsV1::$LOGOUT_PATH, + [], + false, + $refreshToken + ); + return; + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** @@ -187,12 +219,19 @@ public function logoutAll(string $refreshToken = null): void throw new \InvalidArgumentException('Refresh token is required.'); } - $this->api->doPost( - EndpointsV1::LOGOUT_ALL_PATH, - [], - false, - $refreshToken - ); + try { + $this->api->doPost( + EndpointsV1::$LOGOUT_ALL_PATH, + [], + false, + $refreshToken + ); + return; + } catch (RequestException $e) { + $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; + $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; + throw new AuthException($statusCode, 'RequestException', $e->getMessage()); + } } /** diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php new file mode 100644 index 0000000..1ff6b52 --- /dev/null +++ b/src/SDK/Management/Role.php @@ -0,0 +1,95 @@ +api = $api; + } + + /** + * Validates tenant permissions for a JWT response. + * + * @param array $jwtResponse JWT response data. + * @param string $tenant Tenant ID. + * @param array $permissions Permissions to validate. + * @return bool True if tenant permissions are valid, false otherwise. + * @throws AuthException If JWT response is invalid. + */ + public function validateTenantPermissions(array $jwtResponse, string $tenant = '', array $permissions): bool + { + if (!is_array($permissions)) { + $permissions = [$permissions]; + } + + if (!is_array($jwtResponse)) { + throw new AuthException(400, 'Invalid JWT response hash'); + } + + $grantedPermissions = $jwtResponse['permissions'] ?? []; + if (!empty($tenant)) { + if (empty($jwtResponse['tenants'][$tenant])) { + return false; + } + $grantedPermissions = $jwtResponse['tenants'][$tenant]['permissions'] ?? []; + } + + return empty(array_diff($permissions, $grantedPermissions)); + } + + /** + * Validates roles for a JWT response. + * + * @param array $jwtResponse JWT response data. + * @param array $roles Roles to validate. + * @return bool True if roles are valid, false otherwise. + */ + public function validateRoles(array $jwtResponse, array $roles): bool + { + return $this->validateTenantRoles($jwtResponse, '', $roles); + } + + /** + * Validates tenant roles for a JWT response. + * + * @param array $jwtResponse JWT response data. + * @param string $tenant Tenant ID. + * @param array $roles Roles to validate. + * @return bool True if tenant roles are valid, false otherwise. + * @throws AuthException If JWT response is invalid. + */ + public function validateTenantRoles(array $jwtResponse, string $tenant, array $roles): bool + { + if (!is_array($roles)) { + $roles = [$roles]; + } + + if (!is_array($jwtResponse)) { + throw new AuthException(400, 'Invalid JWT response hash'); + } + + $grantedRoles = $jwtResponse['roles'] ?? []; + if (!empty($tenant)) { + if (empty($jwtResponse['tenants'][$tenant])) { + return false; + } + $grantedRoles = $jwtResponse['tenants'][$tenant]['roles'] ?? []; + } + + return empty(array_diff($roles, $grantedRoles)); + } + +} diff --git a/src/SDK/Token/Verifier.php b/src/SDK/Token/Verifier.php index deaddf8..e4c6390 100644 --- a/src/SDK/Token/Verifier.php +++ b/src/SDK/Token/Verifier.php @@ -15,19 +15,22 @@ use Descope\SDK\Token\Extractor; use Descope\SDK\Configuration\SDKConfig; use Descope\SDK\EndpointsV1; +use Descope\SDK\API; final class Verifier { private SDKConfig $config; + private API $api; /** * Constructor for Verifier class. * * @param SDKConfig $config Base configuration options for the SDK. */ - public function __construct($config) + public function __construct(SDKConfig $config, API $api) { $this->config = $config; + $this->api = $api; } /** @@ -60,125 +63,4 @@ public function verify($sessionToken, ?string $audience = null) throw TokenException::MSG_SIGNATURE_INVALID; } } - - /** - * Refreshes the session token, with the provided refresh token. - * - * @param string $refreshToken The refresh token. - * @return array The refreshed JWT response. - * @throws AuthException If the refresh operation fails. - */ - public function refreshSession(string $refreshToken): array - { - $this->validateRefreshTokenNotNil($refreshToken); - $uri = EndpointsV1::$REFRESH_TOKEN_PATH; - $response = $this->doPost($uri, [], $refreshToken); - return $this->generateJwtResponse($response, $refreshToken); - } - - /** - * Verifies the session token, and automatically refreshes when expired. - * - * @param string $sessionToken The session token. - * @param string $refreshToken The refresh token. - * @return array The JWT response. - * @throws AuthException If both tokens are missing or verification fails. - */ - public function verifyAndRefreshSession(string $sessionToken, string $refreshToken): array - { - if (empty($sessionToken)) { - throw new AuthException(400, 'Session token is missing'); - } - - try { - $this->verify($sessionToken); - return $this->generateJwtResponse($sessionToken, $refreshToken); - } catch (AuthException $e) { - return $this->refreshSession($refreshToken); - } - } - - /** - * Validates permissions for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param array $permissions Permissions to validate. - * @return bool True if permissions are valid, false otherwise. - */ - public function validatePermissions(array $jwtResponse, array $permissions): bool - { - return $this->validateTenantPermissions($jwtResponse, '', $permissions); - } - - /** - * Validates tenant permissions for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param string $tenant Tenant ID. - * @param array $permissions Permissions to validate. - * @return bool True if tenant permissions are valid, false otherwise. - * @throws AuthException If JWT response is invalid. - */ - public function validateTenantPermissions(array $jwtResponse, string $tenant, array $permissions): bool - { - if (!is_array($permissions)) { - $permissions = [$permissions]; - } - - if (!is_array($jwtResponse)) { - throw new AuthException(400, 'Invalid JWT response hash'); - } - - $grantedPermissions = $jwtResponse['permissions'] ?? []; - if (!empty($tenant)) { - if (empty($jwtResponse['tenants'][$tenant])) { - return false; - } - $grantedPermissions = $jwtResponse['tenants'][$tenant]['permissions'] ?? []; - } - - return empty(array_diff($permissions, $grantedPermissions)); - } - - /** - * Validates roles for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param array $roles Roles to validate. - * @return bool True if roles are valid, false otherwise. - */ - public function validateRoles(array $jwtResponse, array $roles): bool - { - return $this->validateTenantRoles($jwtResponse, '', $roles); - } - - /** - * Validates tenant roles for a JWT response. - * - * @param array $jwtResponse JWT response data. - * @param string $tenant Tenant ID. - * @param array $roles Roles to validate. - * @return bool True if tenant roles are valid, false otherwise. - * @throws AuthException If JWT response is invalid. - */ - public function validateTenantRoles(array $jwtResponse, string $tenant, array $roles): bool - { - if (!is_array($roles)) { - $roles = [$roles]; - } - - if (!is_array($jwtResponse)) { - throw new AuthException(400, 'Invalid JWT response hash'); - } - - $grantedRoles = $jwtResponse['roles'] ?? []; - if (!empty($tenant)) { - if (empty($jwtResponse['tenants'][$tenant])) { - return false; - } - $grantedRoles = $jwtResponse['tenants'][$tenant]['roles'] ?? []; - } - - return empty(array_diff($roles, $grantedRoles)); - } } diff --git a/src/tests/DescopeSDKTest.php b/src/tests/DescopeSDKTest.php index 3325b50..af6639b 100644 --- a/src/tests/DescopeSDKTest.php +++ b/src/tests/DescopeSDKTest.php @@ -1,45 +1,40 @@ "", - // 'managementKey' => "" // This can be optional - // ] - // ); - } + private $config; + private $sdk; - public function testVerify(): void + protected function setUp(): void { - $token = '...'; - // $this->assertTrue($this->descopeSDK->verify($token)); + $this->config = [ + 'projectId' => 'test_project_id', + 'managementKey' => 'test_management_key' + ]; + $this->sdk = new DescopeSDK($this->config); } - public function getClaims(): void + public function testConstructorInitializesComponents() { - $token = '...'; - // $this->assertNotEmpty($this->descopeSDK->getClaims($token)); + $this->assertInstanceOf(SDKConfig::class, $this->sdk->password()); + $this->assertInstanceOf(SDKConfig::class, $this->sdk->sso()); + $this->assertInstanceOf(SDKConfig::class, $this->sdk->management()); } - public function testUserDetails(): void + public function testVerifyThrowsExceptionWithoutToken() { - $refresh_token = '...'; - // $this->assertIsArray($this->descopeSDK->getUser($refresh_token)); + $this->expectException(\InvalidArgumentException::class); + $this->sdk->verify(null); } - public function testPassword(): void + public function testRefreshSessionThrowsExceptionWithoutToken() { - // $result = $this->descopeSDK->password->signUp("example@descope.com", "Password123!", []); - // $this->assertIsArray($this->descopeSDK->password->signUp("example@descope.com", "Password123!", [])); + $this->expectException(\InvalidArgumentException::class); + $this->sdk->refreshSession(null); } -} +} \ No newline at end of file From 23337515f9c510308b4f39bd578cc2e319a0a151 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:53:06 -0800 Subject: [PATCH 04/21] fixed phpunit.xml --- phpunit.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpunit.xml b/phpunit.xml index 8e6191a..ef67d5b 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,7 +2,7 @@ - tests + src/tests \ No newline at end of file From 957ce8367d69778d977509c2e56cd3909abe998c Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:55:27 -0800 Subject: [PATCH 05/21] fixed linter --- src/SDK/API.php | 42 ++++++++++++++++++------------------ src/SDK/Auth/SSO.php | 12 +++++------ src/SDK/Management/Role.php | 1 - src/tests/DescopeSDKTest.php | 2 +- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/SDK/API.php b/src/SDK/API.php index abd435c..52b4fe6 100644 --- a/src/SDK/API.php +++ b/src/SDK/API.php @@ -20,7 +20,7 @@ class API /** * Constructor for API class. * - * @param string $projectId + * @param string $projectId * @param string|null $managementKey Management key for authentication. */ public function __construct(string $projectId, ?string $managementKey) @@ -52,7 +52,7 @@ public function __construct(string $projectId, ?string $managementKey) * This function ensures that empty arrays in the input data are * converted to empty objects (stdClass) before being JSON encoded. * - * @param mixed $data The data to transform, which can be an array or any other type. + * @param mixed $data The data to transform, which can be an array or any other type. * @return mixed The transformed data with empty arrays replaced by empty objects. */ private function transformEmptyArraysToObjects($data) @@ -76,9 +76,9 @@ private function transformEmptyArraysToObjects($data) /** * Requests JwtResponse from Descope APIs with the given body and auth token. * - * @param string $uri URI endpoint. - * @param array $body Request body. - * @param bool $useManagementKey Whether to use the management key for authentication. + * @param string $uri URI endpoint. + * @param array $body Request body. + * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. * @throws AuthException|GuzzleException|\JsonException If the request fails. */ @@ -130,8 +130,8 @@ public function doPost(string $uri, array $body, ?bool $useManagementKey = false /** * Sends a GET request to the specified URI with an optional auth token. * - * @param string $uri URI endpoint. - * @param bool $useManagementKey Whether to use the management key for authentication. + * @param string $uri URI endpoint. + * @param bool $useManagementKey Whether to use the management key for authentication. * @return array JWT response array. * @throws AuthException|GuzzleException|\JsonException If the request fails. */ @@ -178,7 +178,7 @@ public function doGet(string $uri, bool $useManagementKey, ?string $refreshToken /** * Sends a DELETE request to the specified URI with an auth token. * - * @param string $uri URI endpoint. + * @param string $uri URI endpoint. * @return array JWT response array. * @throws AuthException|GuzzleException|\JsonException If the request fails. */ @@ -219,9 +219,9 @@ public function doDelete(string $uri): array /** * Generates a JWT response array with the given parameters. * - * @param array $responseBody - * @param string|null $refreshToken Refresh token. - * @param string|null $audience Audience. + * @param array $responseBody + * @param string|null $refreshToken Refresh token. + * @param string|null $audience Audience. * @return array JWT response array. */ public function generateJwtResponse(array $responseBody, ?string $refreshToken = null, ?string $audience = null): array @@ -237,7 +237,7 @@ public function generateJwtResponse(array $responseBody, ?string $refreshToken = /** * Generates headers for the HTTP request. * - * @param string|null $authToken Authentication token. + * @param string|null $authToken Authentication token. * @return array Headers array. */ private function getHeaders(string $authToken): array @@ -255,7 +255,7 @@ private function getHeaders(string $authToken): array /** * Constructs the auth token based on whether the management key is used. * - * @param bool $useManagementKey Whether to use the management key for authentication. + * @param bool $useManagementKey Whether to use the management key for authentication. * @return string The constructed auth token. */ private function getAuthToken(bool $useManagementKey, ?string $refreshToken = null): string @@ -277,12 +277,12 @@ private function getAuthToken(bool $useManagementKey, ?string $refreshToken = nu * This method processes the response body to extract JWTs, session data, * and cookie settings, and adjusts properties based on the token type. * - * @param array $responseBody The API response body containing JWTs and user data. - * @param string|null $refreshToken Optional refresh token. - * @param bool $userJwt Indicates if user-related JWT information should be processed. - * @param string|null $audience Optional audience identifier. + * @param array $responseBody The API response body containing JWTs and user data. + * @param string|null $refreshToken Optional refresh token. + * @param bool $userJwt Indicates if user-related JWT information should be processed. + * @param string|null $audience Optional audience identifier. * @return array The structured JWT response array containing session and user data. - */ + */ private function generateAuthInfo(array $responseBody, ?string $refreshToken, bool $userJwt, ?string $audience): array { $jwtResponse = []; @@ -320,10 +320,10 @@ private function generateAuthInfo(array $responseBody, ?string $refreshToken, bo * This method sets permissions, roles, and tenant data from the JWT * and processes the issuer and subject values to extract project and user IDs. * - * @param array $jwtResponse The JWT response array to adjust. - * @param bool $userJwt Indicates if user-related JWT information should be processed. + * @param array $jwtResponse The JWT response array to adjust. + * @param bool $userJwt Indicates if user-related JWT information should be processed. * @return array The adjusted JWT response array with updated properties. - */ + */ private function adjustProperties(array $jwtResponse, bool $userJwt): array { if (isset($jwtResponse[EndpointsV1::$SESSION_TOKEN_NAME])) { diff --git a/src/SDK/Auth/SSO.php b/src/SDK/Auth/SSO.php index f71a803..3b0e29f 100644 --- a/src/SDK/Auth/SSO.php +++ b/src/SDK/Auth/SSO.php @@ -61,7 +61,7 @@ public function signIn(?string $tenant = null, ?string $redirectUrl = null, ?str /** * Exchanges SSO code for authentication. * - * @param string|null $code The exchange code. + * @param string|null $code The exchange code. * @return array Response array. */ public function exchangeToken(?string $code = null): array @@ -77,9 +77,9 @@ public function exchangeToken(?string $code = null): array /** * Composes the SSO sign-in URL. * - * @param string|null $tenant Tenant identifier. - * @param string|null $redirectUrl Redirect URL. - * @param string|null $prompt Prompt parameter. + * @param string|null $tenant Tenant identifier. + * @param string|null $redirectUrl Redirect URL. + * @param string|null $prompt Prompt parameter. * @return string Composed URL. */ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string $prompt): string @@ -104,7 +104,7 @@ private function composeSignInUrl(?string $tenant, ?string $redirectUrl, ?string /** * Validates the tenant parameter. * - * @param string|null $tenant The tenant identifier. + * @param string|null $tenant The tenant identifier. * @throws AuthException */ private function validateTenant(?string $tenant): void @@ -117,7 +117,7 @@ private function validateTenant(?string $tenant): void /** * Validates the redirect URL parameter. * - * @param string|null $redirectUrl The redirect URL. + * @param string|null $redirectUrl The redirect URL. * @throws AuthException */ private function validateRedirectUrl(?string $redirectUrl): void diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php index 1ff6b52..ef074ba 100644 --- a/src/SDK/Management/Role.php +++ b/src/SDK/Management/Role.php @@ -91,5 +91,4 @@ public function validateTenantRoles(array $jwtResponse, string $tenant, array $r return empty(array_diff($roles, $grantedRoles)); } - } diff --git a/src/tests/DescopeSDKTest.php b/src/tests/DescopeSDKTest.php index af6639b..37ebfbb 100644 --- a/src/tests/DescopeSDKTest.php +++ b/src/tests/DescopeSDKTest.php @@ -37,4 +37,4 @@ public function testRefreshSessionThrowsExceptionWithoutToken() $this->expectException(\InvalidArgumentException::class); $this->sdk->refreshSession(null); } -} \ No newline at end of file +} From ff154bf07eeb19c839cfd1738bd996e96d4fbb0a Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 12:30:21 -0800 Subject: [PATCH 06/21] added jwk caching and fully custom JWT validation logic, instead of using Jose JWT --- composer.json | 1 - composer.lock | 2284 ++------------------------- sample/callback.php | 2 +- sample/dashboard.php | 4 - src/SDK/Configuration/SDKConfig.php | 35 +- src/SDK/DescopeSDK.php | 13 +- src/SDK/Token/Extractor.php | 240 ++- src/SDK/Token/Verifier.php | 57 +- 8 files changed, 364 insertions(+), 2272 deletions(-) diff --git a/composer.json b/composer.json index 79020bc..dd29eb7 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ ], "require": { "php": "^7.3 || ^8.0", - "web-token/jwt-framework": "2.2.11 as 2.3.0", "guzzlehttp/guzzle": "7.9.2 as 7.9.3", "paragonie/constant_time_encoding": "^2.7.0", "vlucas/phpdotenv": "^5.6.1" diff --git a/composer.lock b/composer.lock index d26365e..46212a7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,144 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "35e70dbd77fca8551fdaf29f9d79b765", + "content-hash": "445d03808865bcc00bd5191e7902ce33", "packages": [ - { - "name": "brick/math", - "version": "0.9.3", - "source": { - "type": "git", - "url": "https://github.com/brick/math.git", - "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/ca57d18f028f84f777b2168cd1911b0dee2343ae", - "reference": "ca57d18f028f84f777b2168cd1911b0dee2343ae", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.2", - "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", - "vimeo/psalm": "4.9.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Brick\\Math\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Arbitrary-precision arithmetic library", - "keywords": [ - "Arbitrary-precision", - "BigInteger", - "BigRational", - "arithmetic", - "bigdecimal", - "bignum", - "brick", - "math" - ], - "support": { - "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.9.3" - }, - "funding": [ - { - "url": "https://github.com/BenMorel", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/brick/math", - "type": "tidelift" - } - ], - "time": "2021-08-15T20:50:18+00:00" - }, - { - "name": "fgrosse/phpasn1", - "version": "v2.5.0", - "source": { - "type": "git", - "url": "https://github.com/fgrosse/PHPASN1.git", - "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b", - "reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "require-dev": { - "php-coveralls/php-coveralls": "~2.0", - "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" - }, - "suggest": { - "ext-bcmath": "BCmath is the fallback extension for big integer calculations", - "ext-curl": "For loading OID information from the web if they have not bee defined statically", - "ext-gmp": "GMP is the preferred extension for big integer calculations", - "phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "FG\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Friedrich Große", - "email": "friedrich.grosse@gmail.com", - "homepage": "https://github.com/FGrosse", - "role": "Author" - }, - { - "name": "All contributors", - "homepage": "https://github.com/FGrosse/PHPASN1/contributors" - } - ], - "description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.", - "homepage": "https://github.com/FGrosse/PHPASN1", - "keywords": [ - "DER", - "asn.1", - "asn1", - "ber", - "binary", - "decoding", - "encoding", - "x.509", - "x.690", - "x509", - "x690" - ], - "support": { - "issues": "https://github.com/fgrosse/PHPASN1/issues", - "source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0" - }, - "abandoned": true, - "time": "2022-12-19T11:08:26+00:00" - }, { "name": "graham-campbell/result-type", "version": "v1.1.3", @@ -671,104 +535,6 @@ ], "time": "2024-07-20T21:41:07+00:00" }, - { - "name": "psr/container", - "version": "1.1.2", - "source": { - "type": "git", - "url": "https://github.com/php-fig/container.git", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", - "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", - "shasum": "" - }, - "require": { - "php": ">=7.4.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Psr\\Container\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common Container Interface (PHP FIG PSR-11)", - "homepage": "https://github.com/php-fig/container", - "keywords": [ - "PSR-11", - "container", - "container-interface", - "container-interop", - "psr" - ], - "support": { - "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/1.1.2" - }, - "time": "2021-11-05T16:50:12+00:00" - }, - { - "name": "psr/event-dispatcher", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/event-dispatcher.git", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", - "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", - "shasum": "" - }, - "require": { - "php": ">=7.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\EventDispatcher\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" - } - ], - "description": "Standard interfaces for event handling.", - "keywords": [ - "events", - "psr", - "psr-14" - ], - "support": { - "issues": "https://github.com/php-fig/event-dispatcher/issues", - "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" - }, - "time": "2019-01-08T18:20:26+00:00" - }, { "name": "psr/http-client", "version": "1.0.3", @@ -929,56 +695,6 @@ }, "time": "2023-04-04T09:54:51+00:00" }, - { - "name": "psr/log", - "version": "1.1.4", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Psr\\Log\\": "Psr/Log/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" - } - ], - "description": "Common interface for logging libraries", - "homepage": "https://github.com/php-fig/log", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" - }, - "time": "2021-05-03T11:20:27+00:00" - }, { "name": "ralouphie/getallheaders", "version": "3.0.3", @@ -1000,1416 +716,10 @@ "php-coveralls/php-coveralls": "^2.1", "phpunit/phpunit": "^5 || ^6.5" }, - "type": "library", - "autoload": { - "files": [ - "src/getallheaders.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ralph Khattar", - "email": "ralph.khattar@gmail.com" - } - ], - "description": "A polyfill for getallheaders.", - "support": { - "issues": "https://github.com/ralouphie/getallheaders/issues", - "source": "https://github.com/ralouphie/getallheaders/tree/develop" - }, - "time": "2019-03-08T08:55:37+00:00" - }, - { - "name": "spomky-labs/aes-key-wrap", - "version": "v6.0.0", - "source": { - "type": "git", - "url": "https://github.com/Spomky-Labs/aes-key-wrap.git", - "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/aes-key-wrap/zipball/97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", - "reference": "97388255a37ad6fb1ed332d07e61fa2b7bb62e0d", - "shasum": "" - }, - "require": { - "ext-mbstring": "*", - "lib-openssl": "*", - "php": ">=7.2", - "thecodingmachine/safe": "^1.1" - }, - "require-dev": { - "php-coveralls/php-coveralls": "^2.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-beberlei-assert": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^7.0|^8.0|^9.0", - "thecodingmachine/phpstan-safe-rule": "^1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.0.x-dev" - } - }, - "autoload": { - "psr-4": { - "AESKW\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky-Labs/aes-key-wrap/contributors" - } - ], - "description": "AES Key Wrap for PHP.", - "homepage": "https://github.com/Spomky-Labs/aes-key-wrap", - "keywords": [ - "A128KW", - "A192KW", - "A256KW", - "RFC3394", - "RFC5649", - "aes", - "key", - "padding", - "wrap" - ], - "support": { - "issues": "https://github.com/Spomky-Labs/aes-key-wrap/issues", - "source": "https://github.com/Spomky-Labs/aes-key-wrap/tree/v6.0.0" - }, - "time": "2020-08-01T14:07:55+00:00" - }, - { - "name": "spomky-labs/base64url", - "version": "v2.0.4", - "source": { - "type": "git", - "url": "https://github.com/Spomky-Labs/base64url.git", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Spomky-Labs/base64url/zipball/7752ce931ec285da4ed1f4c5aa27e45e097be61d", - "reference": "7752ce931ec285da4ed1f4c5aa27e45e097be61d", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "require-dev": { - "phpstan/extension-installer": "^1.0", - "phpstan/phpstan": "^0.11|^0.12", - "phpstan/phpstan-beberlei-assert": "^0.11|^0.12", - "phpstan/phpstan-deprecation-rules": "^0.11|^0.12", - "phpstan/phpstan-phpunit": "^0.11|^0.12", - "phpstan/phpstan-strict-rules": "^0.11|^0.12" - }, - "type": "library", - "autoload": { - "psr-4": { - "Base64Url\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky-Labs/base64url/contributors" - } - ], - "description": "Base 64 URL Safe Encoding/Decoding PHP Library", - "homepage": "https://github.com/Spomky-Labs/base64url", - "keywords": [ - "base64", - "rfc4648", - "safe", - "url" - ], - "support": { - "issues": "https://github.com/Spomky-Labs/base64url/issues", - "source": "https://github.com/Spomky-Labs/base64url/tree/v2.0.4" - }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - }, - { - "url": "https://www.patreon.com/FlorentMorselli", - "type": "patreon" - } - ], - "time": "2020-11-03T09:10:25+00:00" - }, - { - "name": "symfony/config", - "version": "v5.4.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "d4e1db78421163b98dd9971d247fd0df4a57ee5e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/d4e1db78421163b98dd9971d247fd0df4a57ee5e", - "reference": "d4e1db78421163b98dd9971d247fd0df4a57ee5e", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/filesystem": "^4.4|^5.0|^6.0", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22" - }, - "conflict": { - "symfony/finder": "<4.4" - }, - "require-dev": { - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/messenger": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/yaml": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/yaml": "To use the yaml reference dumper" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Config\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/config/tree/v5.4.40" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-05-31T14:33:22+00:00" - }, - { - "name": "symfony/console", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "5b5a0aa66e3296e303e22490f90f521551835a83" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/5b5a0aa66e3296e303e22490f90f521551835a83", - "reference": "5b5a0aa66e3296e303e22490f90f521551835a83", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/string": "^5.1|^6.0" - }, - "conflict": { - "psr/log": ">=3", - "symfony/dependency-injection": "<4.4", - "symfony/dotenv": "<5.1", - "symfony/event-dispatcher": "<4.4", - "symfony/lock": "<4.4", - "symfony/process": "<4.4" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/log": "^1|^2", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^4.4|^5.0|^6.0", - "symfony/lock": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "", - "symfony/lock": "", - "symfony/process": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Console\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Eases the creation of beautiful and testable command line interfaces", - "homepage": "https://symfony.com", - "keywords": [ - "cli", - "command-line", - "console", - "terminal" - ], - "support": { - "source": "https://github.com/symfony/console/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-20T07:56:40+00:00" - }, - { - "name": "symfony/dependency-injection", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/dependency-injection.git", - "reference": "23eb9f3803a931aef16a65f362a9aeb0640a1374" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/23eb9f3803a931aef16a65f362a9aeb0640a1374", - "reference": "23eb9f3803a931aef16a65f362a9aeb0640a1374", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/container": "^1.1.1", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-php80": "^1.16", - "symfony/polyfill-php81": "^1.22", - "symfony/service-contracts": "^1.1.6|^2" - }, - "conflict": { - "ext-psr": "<1.1|>=2", - "symfony/config": "<5.3", - "symfony/finder": "<4.4", - "symfony/proxy-manager-bridge": "<4.4", - "symfony/yaml": "<4.4.26" - }, - "provide": { - "psr/container-implementation": "1.0", - "symfony/service-implementation": "1.0|2.0" - }, - "require-dev": { - "symfony/config": "^5.3|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/yaml": "^4.4.26|^5.0|^6.0" - }, - "suggest": { - "symfony/config": "", - "symfony/expression-language": "For using expressions in service container configuration", - "symfony/finder": "For using double-star glob patterns or when GLOB_BRACE portability is required", - "symfony/proxy-manager-bridge": "Generate service proxies to lazy load them", - "symfony/yaml": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\DependencyInjection\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Allows you to standardize and centralize the way objects are constructed in your application", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-12T20:01:35+00:00" - }, - { - "name": "symfony/deprecation-contracts", - "version": "v2.5.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", - "shasum": "" - }, - "require": { - "php": ">=7.1" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "files": [ - "function.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "A generic function and convention to trigger deprecation notices", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2023-01-24T14:02:46+00:00" - }, - { - "name": "symfony/error-handler", - "version": "v5.4.42", - "source": { - "type": "git", - "url": "https://github.com/symfony/error-handler.git", - "reference": "db15ba0fd50890156ed40087ccedc7851a1f5b76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/db15ba0fd50890156ed40087ccedc7851a1f5b76", - "reference": "db15ba0fd50890156ed40087ccedc7851a1f5b76", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2|^3", - "symfony/var-dumper": "^4.4|^5.0|^6.0" - }, - "require-dev": { - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/serializer": "^4.4|^5.0|^6.0" - }, - "bin": [ - "Resources/bin/patch-type-declarations" - ], - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\ErrorHandler\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools to manage errors and ease debugging PHP code", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.4.42" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-07-23T12:34:05+00:00" - }, - { - "name": "symfony/event-dispatcher", - "version": "v5.4.40", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a54e2a8a114065f31020d6a89ede83e34c3b27a4", - "reference": "a54e2a8a114065f31020d6a89ede83e34c3b27a4", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/event-dispatcher-contracts": "^2|^3", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/dependency-injection": "<4.4" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^4.4|^5.0|^6.0", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^4.4|^5.0|^6.0", - "symfony/service-contracts": "^1.1|^2|^3", - "symfony/stopwatch": "^4.4|^5.0|^6.0" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.4.40" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-05-31T14:33:22+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v2.5.3", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "540f4c73e87fd0c71ca44a6aa305d024ac68cb73" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/540f4c73e87fd0c71ca44a6aa305d024ac68cb73", - "reference": "540f4c73e87fd0c71ca44a6aa305d024ac68cb73", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/event-dispatcher": "^1" - }, - "suggest": { - "symfony/event-dispatcher-implementation": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v2.5.3" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-01-23T13:51:25+00:00" - }, - { - "name": "symfony/filesystem", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/filesystem.git", - "reference": "76c3818964e9d32be3862c9318ae3ba9aa280ddc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/76c3818964e9d32be3862c9318ae3ba9aa280ddc", - "reference": "76c3818964e9d32be3862c9318ae3ba9aa280ddc", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-mbstring": "~1.8", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "symfony/process": "^5.4|^6.4" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides basic utilities for the filesystem", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-16T14:52:48+00:00" - }, - { - "name": "symfony/http-foundation", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-foundation.git", - "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", - "reference": "ae0d217e5932aa0b70ddb4cf7822cc76d48aee53", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/polyfill-mbstring": "~1.1", - "symfony/polyfill-php80": "^1.16" - }, - "require-dev": { - "predis/predis": "^1.0|^2.0", - "symfony/cache": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.4|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", - "symfony/mime": "^4.4|^5.0|^6.0", - "symfony/rate-limiter": "^5.2|^6.0" - }, - "suggest": { - "symfony/mime": "To use the file extension guesser" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpFoundation\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Defines an object-oriented layer for the HTTP specification", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-15T07:55:06+00:00" - }, - { - "name": "symfony/http-kernel", - "version": "v5.4.44", - "source": { - "type": "git", - "url": "https://github.com/symfony/http-kernel.git", - "reference": "788dcf72d9af7432a886aa3b0c5904d68087ba13" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/788dcf72d9af7432a886aa3b0c5904d68087ba13", - "reference": "788dcf72d9af7432a886aa3b0c5904d68087ba13", - "shasum": "" - }, - "require": { - "php": ">=7.2.5", - "psr/log": "^1|^2", - "symfony/deprecation-contracts": "^2.1|^3", - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/event-dispatcher": "^5.0|^6.0", - "symfony/http-foundation": "^5.4.21|^6.2.7", - "symfony/polyfill-ctype": "^1.8", - "symfony/polyfill-php73": "^1.9", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/browser-kit": "<5.4", - "symfony/cache": "<5.0", - "symfony/config": "<5.0", - "symfony/console": "<4.4", - "symfony/dependency-injection": "<5.3", - "symfony/doctrine-bridge": "<5.0", - "symfony/form": "<5.0", - "symfony/http-client": "<5.0", - "symfony/mailer": "<5.0", - "symfony/messenger": "<5.0", - "symfony/translation": "<5.0", - "symfony/twig-bridge": "<5.0", - "symfony/validator": "<5.0", - "twig/twig": "<2.13" - }, - "provide": { - "psr/log-implementation": "1.0|2.0" - }, - "require-dev": { - "psr/cache": "^1.0|^2.0|^3.0", - "symfony/browser-kit": "^5.4|^6.0", - "symfony/config": "^5.0|^6.0", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/css-selector": "^4.4|^5.0|^6.0", - "symfony/dependency-injection": "^5.3|^6.0", - "symfony/dom-crawler": "^4.4|^5.0|^6.0", - "symfony/expression-language": "^4.4|^5.0|^6.0", - "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-client-contracts": "^1.1|^2|^3", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/routing": "^4.4|^5.0|^6.0", - "symfony/stopwatch": "^4.4|^5.0|^6.0", - "symfony/translation": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2|^3", - "symfony/var-dumper": "^4.4.31|^5.4", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "symfony/browser-kit": "", - "symfony/config": "", - "symfony/console": "", - "symfony/dependency-injection": "" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\HttpKernel\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a structured process for converting a Request into a Response", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.44" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-21T05:47:58+00:00" - }, - { - "name": "symfony/polyfill-ctype", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-ctype": "*" - }, - "suggest": { - "ext-ctype": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Gert de Pagter", - "email": "BackEndTea@gmail.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for ctype functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "ctype", - "polyfill", - "portable" - ], - "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's grapheme_* functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "grapheme", - "intl", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "suggest": { - "ext-intl": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for intl's Normalizer class and related functions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "intl", - "normalizer", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "provide": { - "ext-mbstring": "*" - }, - "suggest": { - "ext-mbstring": "For best performance" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill for the Mbstring extension", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "mbstring", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php73", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "reference": "0f68c03565dcaaf25a890667542e8bd75fe7e5bb", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.31.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.31.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2418,78 +728,47 @@ ], "authors": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], + "description": "A polyfill for getallheaders.", "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2019-03-08T08:55:37+00:00" }, { - "name": "symfony/polyfill-php81", - "version": "v1.31.0", + "name": "symfony/deprecation-contracts", + "version": "v2.5.3", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", + "reference": "80d075412b557d41002320b96a096ca65aa2c98d", "shasum": "" }, "require": { - "php": ">=7.2" + "php": ">=7.1" }, "type": "library", "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" + "function.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2506,16 +785,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" }, "funding": [ { @@ -2531,46 +804,44 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2023-01-24T14:02:46+00:00" }, { - "name": "symfony/service-contracts", - "version": "v2.5.3", + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3" + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/a2329596ddc8fd568900e3fc76cba42489ecc7f3", - "reference": "a2329596ddc8fd568900e3fc76cba42489ecc7f3", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", "shasum": "" }, "require": { - "php": ">=7.2.5", - "psr/container": "^1.1", - "symfony/deprecation-contracts": "^2.1|^3" + "php": ">=7.2" }, - "conflict": { - "ext-psr": "<1.1|>=2" + "provide": { + "ext-ctype": "*" }, "suggest": { - "symfony/service-implementation": "" + "ext-ctype": "For best performance" }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "2.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { + "files": [ + "bootstrap.php" + ], "psr-4": { - "Symfony\\Contracts\\Service\\": "" + "Symfony\\Polyfill\\Ctype\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -2579,26 +850,24 @@ ], "authors": [ { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill for ctype functions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "ctype", + "polyfill", + "portable" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" }, "funding": [ { @@ -2614,50 +883,45 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:04:16+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/string", - "version": "v5.4.44", + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/string.git", - "reference": "832caa16b6d9aac6bf11747315225f5aba384c24" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/832caa16b6d9aac6bf11747315225f5aba384c24", - "reference": "832caa16b6d9aac6bf11747315225f5aba384c24", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-ctype": "~1.8", - "symfony/polyfill-intl-grapheme": "~1.0", - "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "php": ">=7.2" }, - "conflict": { - "symfony/translation-contracts": ">=3.0" + "provide": { + "ext-mbstring": "*" }, - "require-dev": { - "symfony/error-handler": "^4.4|^5.0|^6.0", - "symfony/http-client": "^4.4|^5.0|^6.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0|^6.0" + "suggest": { + "ext-mbstring": "For best performance" }, "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, "autoload": { "files": [ - "Resources/functions.php" + "bootstrap.php" ], "psr-4": { - "Symfony\\Component\\String\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -2673,18 +937,17 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ - "grapheme", - "i18n", - "string", - "unicode", - "utf-8", - "utf8" + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.4.44" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" }, "funding": [ { @@ -2700,56 +963,41 @@ "type": "tidelift" } ], - "time": "2024-09-20T07:56:40+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { - "name": "symfony/var-dumper", - "version": "v5.4.43", + "name": "symfony/polyfill-php80", + "version": "v1.31.0", "source": { "type": "git", - "url": "https://github.com/symfony/var-dumper.git", - "reference": "6be6a6a8af4818564e3726fc65cf936f34743cef" + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6be6a6a8af4818564e3726fc65cf936f34743cef", - "reference": "6be6a6a8af4818564e3726fc65cf936f34743cef", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", "shasum": "" }, "require": { - "php": ">=7.2.5", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "^1.16" - }, - "conflict": { - "symfony/console": "<4.4" - }, - "require-dev": { - "ext-iconv": "*", - "symfony/console": "^4.4|^5.0|^6.0", - "symfony/http-kernel": "^4.4|^5.0|^6.0", - "symfony/process": "^4.4|^5.0|^6.0", - "symfony/uid": "^5.1|^6.0", - "twig/twig": "^2.13|^3.0.4" - }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" + "php": ">=7.2" }, - "bin": [ - "Resources/bin/var-dump-server" - ], "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, "autoload": { "files": [ - "Resources/functions/dump.php" + "bootstrap.php" ], "psr-4": { - "Symfony\\Component\\VarDumper\\": "" + "Symfony\\Polyfill\\Php80\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2757,6 +1005,10 @@ "MIT" ], "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, { "name": "Nicolas Grekas", "email": "p@tchwork.com" @@ -2766,14 +1018,16 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "debug", - "dump" + "compatibility", + "polyfill", + "portable", + "shim" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.43" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" }, "funding": [ { @@ -2789,146 +1043,7 @@ "type": "tidelift" } ], - "time": "2024-08-30T16:01:46+00:00" - }, - { - "name": "thecodingmachine/safe", - "version": "v1.3.3", - "source": { - "type": "git", - "url": "https://github.com/thecodingmachine/safe.git", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "reference": "a8ab0876305a4cdaef31b2350fcb9811b5608dbc", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "require-dev": { - "phpstan/phpstan": "^0.12", - "squizlabs/php_codesniffer": "^3.2", - "thecodingmachine/phpstan-strict-rules": "^0.12" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1-dev" - } - }, - "autoload": { - "files": [ - "deprecated/apc.php", - "deprecated/libevent.php", - "deprecated/mssql.php", - "deprecated/stats.php", - "lib/special_cases.php", - "generated/apache.php", - "generated/apcu.php", - "generated/array.php", - "generated/bzip2.php", - "generated/calendar.php", - "generated/classobj.php", - "generated/com.php", - "generated/cubrid.php", - "generated/curl.php", - "generated/datetime.php", - "generated/dir.php", - "generated/eio.php", - "generated/errorfunc.php", - "generated/exec.php", - "generated/fileinfo.php", - "generated/filesystem.php", - "generated/filter.php", - "generated/fpm.php", - "generated/ftp.php", - "generated/funchand.php", - "generated/gmp.php", - "generated/gnupg.php", - "generated/hash.php", - "generated/ibase.php", - "generated/ibmDb2.php", - "generated/iconv.php", - "generated/image.php", - "generated/imap.php", - "generated/info.php", - "generated/ingres-ii.php", - "generated/inotify.php", - "generated/json.php", - "generated/ldap.php", - "generated/libxml.php", - "generated/lzf.php", - "generated/mailparse.php", - "generated/mbstring.php", - "generated/misc.php", - "generated/msql.php", - "generated/mysql.php", - "generated/mysqli.php", - "generated/mysqlndMs.php", - "generated/mysqlndQc.php", - "generated/network.php", - "generated/oci8.php", - "generated/opcache.php", - "generated/openssl.php", - "generated/outcontrol.php", - "generated/password.php", - "generated/pcntl.php", - "generated/pcre.php", - "generated/pdf.php", - "generated/pgsql.php", - "generated/posix.php", - "generated/ps.php", - "generated/pspell.php", - "generated/readline.php", - "generated/rpminfo.php", - "generated/rrd.php", - "generated/sem.php", - "generated/session.php", - "generated/shmop.php", - "generated/simplexml.php", - "generated/sockets.php", - "generated/sodium.php", - "generated/solr.php", - "generated/spl.php", - "generated/sqlsrv.php", - "generated/ssdeep.php", - "generated/ssh2.php", - "generated/stream.php", - "generated/strings.php", - "generated/swoole.php", - "generated/uodbc.php", - "generated/uopz.php", - "generated/url.php", - "generated/var.php", - "generated/xdiff.php", - "generated/xml.php", - "generated/xmlrpc.php", - "generated/yaml.php", - "generated/yaz.php", - "generated/zip.php", - "generated/zlib.php" - ], - "psr-4": { - "Safe\\": [ - "lib/", - "deprecated/", - "generated/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "support": { - "issues": "https://github.com/thecodingmachine/safe/issues", - "source": "https://github.com/thecodingmachine/safe/tree/v1.3.3" - }, - "time": "2020-10-28T17:51:34+00:00" + "time": "2024-09-09T11:45:10+00:00" }, { "name": "vlucas/phpdotenv", @@ -3013,183 +1128,6 @@ } ], "time": "2024-07-20T21:52:34+00:00" - }, - { - "name": "web-token/jwt-framework", - "version": "v2.2.11", - "source": { - "type": "git", - "url": "https://github.com/web-token/jwt-framework.git", - "reference": "643cced197e32471418bd89e7a44b69fd04eb9de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/web-token/jwt-framework/zipball/643cced197e32471418bd89e7a44b69fd04eb9de", - "reference": "643cced197e32471418bd89e7a44b69fd04eb9de", - "shasum": "" - }, - "require": { - "brick/math": "^0.8.17|^0.9", - "ext-json": "*", - "ext-mbstring": "*", - "ext-openssl": "*", - "ext-sodium": "*", - "fgrosse/phpasn1": "^2.0", - "php": ">=7.2", - "psr/event-dispatcher": "^1.0", - "psr/http-client": "^1.0", - "psr/http-factory": "^1.0", - "spomky-labs/aes-key-wrap": "^5.0|^6.0", - "spomky-labs/base64url": "^1.0|^2.0", - "symfony/config": "^4.2|^5.0", - "symfony/console": "^4.2|^5.0", - "symfony/dependency-injection": "^4.2|^5.0", - "symfony/event-dispatcher": "^4.2|^5.0", - "symfony/http-kernel": "^4.2|^5.0", - "symfony/polyfill-mbstring": "^1.12" - }, - "conflict": { - "spomky-labs/jose": "*" - }, - "replace": { - "web-token/encryption-pack": "self.version", - "web-token/jwt-bundle": "self.version", - "web-token/jwt-checker": "self.version", - "web-token/jwt-console": "self.version", - "web-token/jwt-core": "self.version", - "web-token/jwt-easy": "self.version", - "web-token/jwt-encryption": "self.version", - "web-token/jwt-encryption-algorithm-aescbc": "self.version", - "web-token/jwt-encryption-algorithm-aesgcm": "self.version", - "web-token/jwt-encryption-algorithm-aesgcmkw": "self.version", - "web-token/jwt-encryption-algorithm-aeskw": "self.version", - "web-token/jwt-encryption-algorithm-dir": "self.version", - "web-token/jwt-encryption-algorithm-ecdh-es": "self.version", - "web-token/jwt-encryption-algorithm-experimental": "self.version", - "web-token/jwt-encryption-algorithm-pbes2": "self.version", - "web-token/jwt-encryption-algorithm-rsa": "self.version", - "web-token/jwt-key-mgmt": "self.version", - "web-token/jwt-nested-token": "self.version", - "web-token/jwt-signature": "self.version", - "web-token/jwt-signature-algorithm-ecdsa": "self.version", - "web-token/jwt-signature-algorithm-eddsa": "self.version", - "web-token/jwt-signature-algorithm-experimental": "self.version", - "web-token/jwt-signature-algorithm-hmac": "self.version", - "web-token/jwt-signature-algorithm-none": "self.version", - "web-token/jwt-signature-algorithm-rsa": "self.version", - "web-token/jwt-util-ecc": "self.version", - "web-token/signature-pack": "self.version" - }, - "require-dev": { - "bjeavons/zxcvbn-php": "^1.0", - "blackfire/php-sdk": "^1.14", - "ext-curl": "*", - "ext-gmp": "*", - "friendsofphp/php-cs-fixer": "^2.16", - "infection/infection": "^0.15|^0.16|^0.17|^0.18|^0.19|^0.20", - "matthiasnoback/symfony-config-test": "^3.1|^4.0", - "nyholm/psr7": "^1.3", - "php-coveralls/php-coveralls": "^2.0", - "php-http/mock-client": "^1.0", - "phpstan/phpstan": "^0.12", - "phpstan/phpstan-deprecation-rules": "^0.12", - "phpstan/phpstan-phpunit": "^0.12", - "phpstan/phpstan-strict-rules": "^0.12", - "phpunit/phpunit": "^8.0|^9.0", - "symfony/browser-kit": "^4.2|^5.0", - "symfony/finder": "^4.2|^5.0", - "symfony/framework-bundle": "^4.2|^5.0", - "symfony/http-client": "^5.2", - "symfony/phpunit-bridge": "^4.2|^5.0", - "symfony/serializer": "^4.2|^5.0", - "symfony/var-dumper": "^4.2|^5.0" - }, - "suggest": { - "bjeavons/zxcvbn-php": "Adds key quality check for oct keys.", - "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", - "php-http/httplug": "To enable JKU/X5U support.", - "php-http/httplug-bundle": "To enable JKU/X5U support.", - "php-http/message-factory": "To enable JKU/X5U support.", - "symfony/serializer": "Use the Symfony serializer to serialize/unserialize JWS and JWE tokens.", - "symfony/var-dumper": "Used to show data on the debug toolbar." - }, - "type": "symfony-bundle", - "autoload": { - "psr-4": { - "Jose\\": "src/", - "Jose\\Component\\Core\\Util\\Ecc\\": [ - "src/Ecc" - ], - "Jose\\Component\\Signature\\Algorithm\\": [ - "src/SignatureAlgorithm/ECDSA", - "src/SignatureAlgorithm/EdDSA", - "src/SignatureAlgorithm/HMAC", - "src/SignatureAlgorithm/None", - "src/SignatureAlgorithm/RSA", - "src/SignatureAlgorithm/Experimental" - ], - "Jose\\Component\\Encryption\\Algorithm\\": [ - "src/EncryptionAlgorithm/Experimental" - ], - "Jose\\Component\\Encryption\\Algorithm\\KeyEncryption\\": [ - "src/EncryptionAlgorithm/KeyEncryption/AESGCMKW", - "src/EncryptionAlgorithm/KeyEncryption/AESKW", - "src/EncryptionAlgorithm/KeyEncryption/Direct", - "src/EncryptionAlgorithm/KeyEncryption/ECDHES", - "src/EncryptionAlgorithm/KeyEncryption/PBES2", - "src/EncryptionAlgorithm/KeyEncryption/RSA" - ], - "Jose\\Component\\Encryption\\Algorithm\\ContentEncryption\\": [ - "src/EncryptionAlgorithm/ContentEncryption/AESGCM", - "src/EncryptionAlgorithm/ContentEncryption/AESCBC" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Florent Morselli", - "homepage": "https://github.com/Spomky" - }, - { - "name": "All contributors", - "homepage": "https://github.com/web-token/jwt-framework/contributors" - } - ], - "description": "JSON Object Signing and Encryption library for PHP and Symfony Bundle.", - "homepage": "https://github.com/web-token/jwt-framework", - "keywords": [ - "JOSE", - "JWE", - "JWK", - "JWKSet", - "JWS", - "Jot", - "RFC7515", - "RFC7516", - "RFC7517", - "RFC7518", - "RFC7519", - "RFC7520", - "bundle", - "jwa", - "jwt", - "symfony" - ], - "support": { - "issues": "https://github.com/web-token/jwt-framework/issues", - "source": "https://github.com/web-token/jwt-framework/tree/v2.2.11" - }, - "funding": [ - { - "url": "https://github.com/Spomky", - "type": "github" - } - ], - "time": "2021-06-25T15:59:52+00:00" } ], "packages-dev": [ @@ -5021,12 +2959,6 @@ "version": "7.9.2.0", "alias": "7.9.3", "alias_normalized": "7.9.3.0" - }, - { - "package": "web-token/jwt-framework", - "version": "2.2.11.0", - "alias": "2.3.0", - "alias_normalized": "2.3.0.0" } ], "minimum-stability": "stable", diff --git a/sample/callback.php b/sample/callback.php index 7e1da49..3c9f0d1 100644 --- a/sample/callback.php +++ b/sample/callback.php @@ -35,7 +35,7 @@ error_log("Session token verification failed."); $descopeSDK->logout(); // Redirect to login page - header('Location: login.php'); + // header('Location: login.php'); exit(); } } else { diff --git a/sample/dashboard.php b/sample/dashboard.php index a8e176e..f412ecb 100644 --- a/sample/dashboard.php +++ b/sample/dashboard.php @@ -16,10 +16,6 @@ // Get user details and session token from session variables $user = $_SESSION["user"]; $sessionToken = $_SESSION["sessionToken"]; - -$descopeSDK = new DescopeSDK([ - 'projectId' => $_ENV['DESCOPE_PROJECT_ID'] -]); ?> diff --git a/src/SDK/Configuration/SDKConfig.php b/src/SDK/Configuration/SDKConfig.php index cd53df4..4877187 100644 --- a/src/SDK/Configuration/SDKConfig.php +++ b/src/SDK/Configuration/SDKConfig.php @@ -14,6 +14,7 @@ final class SDKConfig public $projectId; public $managementKey; public $jwkSets; + private $cachedJWKSets = null; public function __construct(array $config) { @@ -22,24 +23,40 @@ public function __construct(array $config) $this->managementKey = $config['managementKey'] ?? ''; EndpointsV2::setBaseUrl($config['projectId']); + } + + /** + * Gets the current JWK KeySet. Fetches a new one if not cached or if explicitly requested. + */ + public function getJWKSets(bool $forceRefresh = false): array + { + // Return cached JWK if it exists and no refresh is requested + if ($this->cachedJWKSets !== null && !$forceRefresh) { + return $this->cachedJWKSets; + } - $this->jwkSets = $this->getJWKSets(); + // Fetch new JWK KeySet from Descope API + $this->cachedJWKSets = $this->fetchJWKSets(); + return $this->cachedJWKSets; } /** - * Gets the current JWK KeySet that will be needed to validate the JWT + * Fetch the JWK KeySet from the Descope API. */ - private function getJWKSets() + private function fetchJWKSets(): array { try { $url = EndpointsV2::getPublicKeyPath() . '/' . $this->projectId; - - // Fetch JWK public key from Descope API - $res = $this->client->request('GET', $url); - $jwkSets = json_decode($res->getBody(), true); + $response = $this->client->request('GET', $url); + $jwkSets = json_decode($response->getBody(), true); + + if (!isset($jwkSets['keys']) || !is_array($jwkSets['keys'])) { + throw new \Exception('Invalid JWK response'); + } + return $jwkSets; - } catch (RequestException $re) { - return $re; + } catch (RequestException $e) { + throw new \Exception('Failed to fetch JWK KeySet: ' . $e->getMessage()); } } } diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 2313818..4a48677 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -60,7 +60,7 @@ public function __construct(array $config) * @return bool Verification result. * @throws AuthException */ - public function verify($sessionToken = null) + public function verify($sessionToken = null): bool { $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; @@ -79,7 +79,7 @@ public function verify($sessionToken = null) * @return array The new session information. * @throws AuthException */ - public function refreshSession($refreshToken = null) + public function refreshSession($refreshToken = null): array { $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; @@ -109,7 +109,7 @@ public function refreshSession($refreshToken = null) * @return array The refreshed session information. * @throws AuthException */ - public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null) + public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null): array { $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; @@ -133,7 +133,7 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu * @return array The JWT claims. * @throws AuthException */ - public function getClaims($token = null) + public function getClaims($token = null): array { $token = $token ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; @@ -152,7 +152,7 @@ public function getClaims($token = null) * @return array The user details. * @throws AuthException */ - public function getUserDetails(string $refreshToken = null) + public function getUserDetails(string $refreshToken = null): array { $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; @@ -161,12 +161,11 @@ public function getUserDetails(string $refreshToken = null) } try { - $this->api->doGet( + return $this->api->doGet( EndpointsV1::$ME_PATH, false, $refreshToken ); - return; } catch (RequestException $e) { $statusCode = $e->getResponse() ? $e->getResponse()->getStatusCode() : 'N/A'; $responseBody = $e->getResponse() ? $e->getResponse()->getBody()->getContents() : 'No response body'; diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index fcee856..3dc3edb 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -5,13 +5,6 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Request; -use Jose\Component\Core\Util\JsonConverter; -use Jose\Component\Core\AlgorithmManager; -use Jose\Component\Core\JWKSet; -use Jose\Component\Signature\Algorithm\RS256; -use Jose\Component\Signature\JWSVerifier; -use Jose\Component\Signature\Serializer\CompactSerializer; -use Jose\Component\Signature\Serializer\JWSSerializerManager; use Descope\SDK\Exception\TokenException; use Descope\SDK\Configuration\SDKConfig; @@ -32,68 +25,211 @@ public function __construct($config) /** * Return an array representing the Token's claims. * - * @return array|int|string> + * @return array */ - public function getClaims($sessionToken): array + public function getClaims(string $sessionToken): array { - $jws = $this->parseToken($sessionToken); - return json_decode($jws->getPayload(), true) ?? []; + $parts = $this->parseToken($sessionToken); + return $parts['payload'] ?? []; } /** - * Return all user information using /auth/me API endpoint. + * Parse and validate the JWT token. * - * @return json + * @throws TokenException if validation fails. */ - public function getUserDetails($refreshToken) + public function parseToken(string $sessionToken): array { - $client = $this->config->client; - $url = EndpointsV1::$ME_PATH; - $header = 'Bearer ' . $this->config->projectId . ":" . $refreshToken; + $parts = explode('.', $sessionToken); + if (count($parts) !== 3) { + throw new TokenException('Invalid JWT format'); + } - try { - $response = $client->get($url, ['headers' => ['Authorization' => $header]]); - return json_decode($response->getBody(), true); - } catch (RequestException $e) { - throw new TokenException('Failed to retrieve user details: ' . $e->getMessage()); + $header = $this->decodeJWTPart($parts[0]); + $payload = $this->decodeJWTPart($parts[1]); + $signature = $this->base64UrlDecode($parts[2]); + + if (!isset($header['alg']) || $header['alg'] !== 'RS256') { + throw new TokenException('Unsupported algorithm. Only RS256 is supported.'); } + + return [ + 'raw' => [ + 'header' => $parts[0], + 'payload' => $parts[1], + 'signature' => $parts[2] + ], + 'header' => $header, + 'payload' => $payload, + 'signature' => $signature + ]; } /** - * Parse a JWT string, returning the JWS Object with all of the claims if valid signature. - * - * @throws TokenException if signature verification fails or parsing fails. + * Validate a JWT using the provided JWK Set. + */ + public function validateJWT(string $sessionToken): array + { + $useRefreshedKey = false; + do { + try { + $jwkSet = $this->config->getJWKSets($useRefreshedKey); + $jwt = $this->parseToken($sessionToken); + + if (!isset($jwt['header']['kid'])) { + throw new TokenException('Missing key ID in JWT header'); + } + + $matchingKey = null; + foreach ($jwkSet['keys'] as $key) { + if ($key['kid'] === $jwt['header']['kid']) { + $matchingKey = $key; + break; + } + } + + if (!$matchingKey) { + throw new TokenException('No matching key found in JWKS'); + } + + $publicKeyPEM = $this->convertJWKToPEM($matchingKey); + $signatureValid = $this->verifySignature( + $jwt['raw']['header'] . '.' . $jwt['raw']['payload'], + $jwt['signature'], + $publicKeyPEM + ); + + if (!$signatureValid) { + throw new TokenException('Invalid signature'); + } + + return $jwt['payload']; + + } catch (TokenException $e) { + if ($useRefreshedKey) { + throw new TokenException('JWT validation failed after retry: ' . $e->getMessage()); + } + $useRefreshedKey = true; + } + } while ($useRefreshedKey); + + throw new TokenException('JWT validation failed'); + } + + /** + * Verify JWT signature. */ - public function parseToken($sessionToken) + private function verifySignature(string $signedData, string $signature, string $publicKeyPEM): bool { + $publicKey = openssl_pkey_get_public($publicKeyPEM); + if (!$publicKey) { + throw new TokenException('Invalid public key'); + } + try { - $jwkSets = $this->config->jwkSets; - $jwkSet = JWKSet::createFromKeyData($jwkSets); - - $jwsVerifier = new JWSVerifier( - new AlgorithmManager( - [ - new RS256(), - ] - ) + $result = openssl_verify( + $signedData, + $signature, + $publicKey, + OPENSSL_ALGO_SHA256 ); - - $serializerManager = new JWSSerializerManager( - [ - new CompactSerializer(), - ] - ); - - $jws = $serializerManager->unserialize($sessionToken); - - $isVerified = $jwsVerifier->verifyWithKeySet($jws, $jwkSet, 0); - if ($isVerified) { - return $jws; - } else { - throw new TokenException(TokenException::MSG_SIGNATURE_INVALID); - } - } catch (Exception $e) { - throw new TokenException(TokenException::MSG_COULD_NOT_PARSE); + + return $result === 1; + } finally { + openssl_free_key($publicKey); + } + } + + /** + * Convert JWK to PEM format. + */ + private function convertJWKToPEM(array $jwk): string + { + if (!isset($jwk['kty']) || $jwk['kty'] !== 'RSA') { + throw new TokenException('Invalid key type. Only RSA is supported.'); + } + + $modulus = $this->base64UrlDecode($jwk['n']); + $exponent = $this->base64UrlDecode($jwk['e']); + + // Remove leading null bytes from modulus + $modulus = ltrim($modulus, "\x00"); + + // Construct RSA public key in ASN.1 format + $modulus = pack('Ca*a*', 0x02, $this->encodeLength(strlen($modulus)), $modulus); + $exponent = pack('Ca*a*', 0x02, $this->encodeLength(strlen($exponent)), $exponent); + + $rsaPublicKey = pack( + 'Ca*a*', + 0x30, + $this->encodeLength(strlen($modulus . $exponent)), + $modulus . $exponent + ); + + // Add RSA public key algorithm identifier + $algorithmIdentifier = pack('H*', '300d06092a864886f70d0101010500'); + $bitString = pack('Ca*', 0x03, $this->encodeLength(strlen($rsaPublicKey) + 1) . "\x00" . $rsaPublicKey); + + $der = pack( + 'Ca*a*', + 0x30, + $this->encodeLength(strlen($algorithmIdentifier . $bitString)), + $algorithmIdentifier . $bitString + ); + + return sprintf( + "-----BEGIN PUBLIC KEY-----\n%s-----END PUBLIC KEY-----\n", + chunk_split(base64_encode($der), 64, "\n") + ); + } + + + /** + * Helper to encode the length in DER format. + */ + private function encodeLength(int $length): string + { + if ($length < 128) { + return chr($length); + } + + $temp = $length; + $bytes = ''; + while ($temp > 0) { + $bytes = chr($temp & 0xFF) . $bytes; + $temp >>= 8; + } + return chr(0x80 | strlen($bytes)) . $bytes; + } + + /** + * Decodes a Base64Url-encoded string. + */ + private function base64UrlDecode(string $data): string + { + $padded = str_pad( + strtr($data, '-_', '+/'), + strlen($data) + (4 - strlen($data) % 4) % 4, + '=' + ); + + $decoded = base64_decode($padded, true); + if ($decoded === false) { + throw new TokenException('Invalid base64url encoding'); + } + + return $decoded; + } + + private function decodeJWTPart(string $data): array + { + $decoded = $this->base64UrlDecode($data); + $result = json_decode($decoded, true); + + if (JSON_ERROR_NONE !== json_last_error()) { + throw new TokenException('Invalid JWT part encoding'); } + + return $result; } } diff --git a/src/SDK/Token/Verifier.php b/src/SDK/Token/Verifier.php index e4c6390..a5e07e7 100644 --- a/src/SDK/Token/Verifier.php +++ b/src/SDK/Token/Verifier.php @@ -4,14 +4,8 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; +use Descope\SDK\Exception\TokenException; use GuzzleHttp\Psr7\Request; -use Jose\Component\Core\Util\JsonConverter; -use Jose\Component\Core\AlgorithmManager; -use Jose\Component\Core\JWKSet; -use Jose\Component\Signature\Algorithm\RS256; -use Jose\Component\Signature\JWSVerifier; -use Jose\Component\Signature\Serializer\CompactSerializer; -use Jose\Component\Signature\Serializer\JWSSerializerManager; use Descope\SDK\Token\Extractor; use Descope\SDK\Configuration\SDKConfig; use Descope\SDK\EndpointsV1; @@ -31,6 +25,7 @@ public function __construct(SDKConfig $config, API $api) { $this->config = $config; $this->api = $api; + $this->extractor = new Extractor($this->config); } /** @@ -40,27 +35,45 @@ public function __construct(SDKConfig $config, API $api) * @return boolean Token signature is valid and not expired. * @throws AuthException If the refresh operation fails. */ - public function verify($sessionToken, ?string $audience = null) + public function verify(string $sessionToken, ?string $audience = null): bool { try { - $extractor = new Extractor($this->config); - $jws = $extractor->parseToken($sessionToken); - - // If JWT signature is valid - if (isset($jws)) { - $payload = json_decode($jws->getPayload()); + // First validate the token signature + $validatedToken = $this->extractor->validateJWT($sessionToken); + if (!$validatedToken) { + throw new TokenException('Invalid token signature'); + } + + // Verify expiration + if (isset($validatedToken['exp']) && time() > $validatedToken['exp']) { + throw new TokenException('Token has expired'); + } - // Check to make sure JWT is not expired - if (isset($payload->exp) && time() < $payload->exp) { - if ($audience && (!isset($payload->aud) || $payload->aud !== $audience)) { - return false; + // Verify audience if provided + if ($audience !== null) { + if (!isset($validatedToken['aud'])) { + throw new TokenException('Token is missing audience claim'); + } + + // Handle both string and array audience claims + $tokenAudience = $validatedToken['aud']; + if (is_array($tokenAudience)) { + if (!in_array($audience, $tokenAudience, true)) { + throw new TokenException('Token audience does not match expected value'); + } + } else { + if ($tokenAudience !== $audience) { + throw new TokenException('Token audience does not match expected value'); } - return true; } } - return false; - } catch (TokenException $te) { - throw TokenException::MSG_SIGNATURE_INVALID; + + // All validations passed + return true; + + } catch (TokenException $e) { + // You might want to throw a specific error or return false depending on your needs + throw new TokenException('Token validation failed: ' . $e->getMessage()); } } } From ac32f8682adb657b41a6d71562eedf713bcb35d2 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:14:39 -0800 Subject: [PATCH 07/21] added MD5 hashing algo support --- .phpunit.result.cache | 1 + src/SDK/DescopeSDK.php | 31 +++++++------- .../Management/Password/UserPasswordMD5.php | 41 +++++++++++++++++++ src/SDK/Management/Role.php | 4 +- src/SDK/Token/Extractor.php | 2 +- src/tests/DescopeSDKTest.php | 14 ++++--- src/tests/Management/UserPwdTest.php | 14 +++++++ src/tests/Management/UserTest.php | 3 -- 8 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 .phpunit.result.cache create mode 100644 src/SDK/Management/Password/UserPasswordMD5.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache new file mode 100644 index 0000000..de413ee --- /dev/null +++ b/.phpunit.result.cache @@ -0,0 +1 @@ +{"version":1,"defects":{"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordMD5":4},"times":{"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordBcrypt":0.004,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordFirebase":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordPbkdf2":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordDjango":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordMD5":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordWithCleartext":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordWithHashedPassword":0}} \ No newline at end of file diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 4a48677..2bf29c2 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -13,6 +13,7 @@ use Descope\SDK\Auth\Management\Audit; use Descope\SDK\EndpointsV1; use Descope\SDK\EndpointsV2; +use Descope\SDK\Exception\AuthException; use Descope\SDK\Management\MgmtV1; @@ -62,10 +63,10 @@ public function __construct(array $config) */ public function verify($sessionToken = null): bool { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$sessionToken) { - throw new \InvalidArgumentException('Session token is required.'); + throw new \InvalidArgumentException('Session token cannot be null or empty.'); } $verifier = new Verifier($this->config, $this->api); @@ -81,10 +82,10 @@ public function verify($sessionToken = null): bool */ public function refreshSession($refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (empty($refreshToken)) { - throw new AuthException('Refresh token cannot be null or empty.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { @@ -111,11 +112,11 @@ public function refreshSession($refreshToken = null): array */ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null): array { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (empty($sessionToken) || empty($refreshToken)) { - throw new AuthException(400, 'Session or refresh token cannot be null or empty.'); + throw new \InvalidArgumentException('Session or refresh token cannot be null or empty.'); } try { @@ -135,10 +136,10 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu */ public function getClaims($token = null): array { - $token = $token ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; + $token = $token ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$token) { - throw new \InvalidArgumentException('Token is required.'); + throw new \InvalidArgumentException('Session token cannot be null or empty.'); } $extractor = new Extractor($this->config); @@ -154,10 +155,10 @@ public function getClaims($token = null): array */ public function getUserDetails(string $refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { @@ -182,10 +183,10 @@ public function getUserDetails(string $refreshToken = null): array */ public function logout(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { @@ -212,10 +213,10 @@ public function logout(string $refreshToken = null): void */ public function logoutAll(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { diff --git a/src/SDK/Management/Password/UserPasswordMD5.php b/src/SDK/Management/Password/UserPasswordMD5.php new file mode 100644 index 0000000..a83ca3e --- /dev/null +++ b/src/SDK/Management/Password/UserPasswordMD5.php @@ -0,0 +1,41 @@ +hash = $hash; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'md5' => [ + 'hash' => $this->hash, + ], + ]; + } +} \ No newline at end of file diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php index ef074ba..5e822be 100644 --- a/src/SDK/Management/Role.php +++ b/src/SDK/Management/Role.php @@ -29,8 +29,10 @@ public function __construct(API $api) * @return bool True if tenant permissions are valid, false otherwise. * @throws AuthException If JWT response is invalid. */ - public function validateTenantPermissions(array $jwtResponse, string $tenant = '', array $permissions): bool + public function validateTenantPermissions(array $jwtResponse, array $permissions, ?string $tenant = null): bool { + $tenant = $tenant ?? ''; + if (!is_array($permissions)) { $permissions = [$permissions]; } diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index 3dc3edb..ad76a74 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -67,7 +67,7 @@ public function parseToken(string $sessionToken): array /** * Validate a JWT using the provided JWK Set. - */ + */ public function validateJWT(string $sessionToken): array { $useRefreshedKey = false; diff --git a/src/tests/DescopeSDKTest.php b/src/tests/DescopeSDKTest.php index 37ebfbb..22e0d85 100644 --- a/src/tests/DescopeSDKTest.php +++ b/src/tests/DescopeSDKTest.php @@ -1,11 +1,15 @@ assertInstanceOf(SDKConfig::class, $this->sdk->password()); - $this->assertInstanceOf(SDKConfig::class, $this->sdk->sso()); - $this->assertInstanceOf(SDKConfig::class, $this->sdk->management()); + $this->assertInstanceOf(Password::class, $this->sdk->password()); + $this->assertInstanceOf(SSO::class, $this->sdk->sso()); + $this->assertInstanceOf(Management::class, $this->sdk->management()); } public function testVerifyThrowsExceptionWithoutToken() diff --git a/src/tests/Management/UserPwdTest.php b/src/tests/Management/UserPwdTest.php index ec6eeb6..aa3ace7 100644 --- a/src/tests/Management/UserPwdTest.php +++ b/src/tests/Management/UserPwdTest.php @@ -8,6 +8,7 @@ use Descope\SDK\Management\Password\UserPasswordFirebase; use Descope\SDK\Management\Password\UserPasswordPbkdf2; use Descope\SDK\Management\Password\UserPasswordDjango; +use Descope\SDK\Management\Password\UserPasswordMD5; class UserPwdTest extends TestCase { @@ -81,6 +82,19 @@ public function testUserPasswordDjango() $this->assertEquals($expectedArray, $userPasswordDjango->toArray()); } + public function testUserPasswordMD5() + { + $md5Hash = 'pbkdf2_sha256$30000$hashvalue'; + $userPasswordMD5 = new UserPasswordMD5($md5Hash); + $expectedArray = [ + 'md5' => [ + 'hash' => $md5Hash, + ], + ]; + + $this->assertEquals($expectedArray, $userPasswordMD5->toArray()); + } + public function testUserPasswordWithCleartext() { $cleartextPassword = 'mypassword'; diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index 19ecf65..0ba158f 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -6,9 +6,6 @@ use Descope\SDK\DescopeSDK; use Descope\SDK\Management\Password\UserPassword; use Descope\SDK\Management\Password\UserPasswordBcrypt; -use Descope\SDK\Management\Password\UserPasswordFirebase; -use Descope\SDK\Management\Password\UserPasswordPbkdf2; -use Descope\SDK\Management\Password\UserPasswordDjango; use Descope\SDK\Management\User; use Descope\SDK\Management\AssociatedTenant; use Descope\SDK\Management\UserObj; From 25878e9209c1d20dc8229a859ea71e0e2ba65b3d Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:14:53 -0800 Subject: [PATCH 08/21] Revert "added MD5 hashing algo support" This reverts commit ac32f8682adb657b41a6d71562eedf713bcb35d2. --- .phpunit.result.cache | 1 - src/SDK/DescopeSDK.php | 31 +++++++------- .../Management/Password/UserPasswordMD5.php | 41 ------------------- src/SDK/Management/Role.php | 4 +- src/SDK/Token/Extractor.php | 2 +- src/tests/DescopeSDKTest.php | 14 +++---- src/tests/Management/UserPwdTest.php | 14 ------- src/tests/Management/UserTest.php | 3 ++ 8 files changed, 25 insertions(+), 85 deletions(-) delete mode 100644 .phpunit.result.cache delete mode 100644 src/SDK/Management/Password/UserPasswordMD5.php diff --git a/.phpunit.result.cache b/.phpunit.result.cache deleted file mode 100644 index de413ee..0000000 --- a/.phpunit.result.cache +++ /dev/null @@ -1 +0,0 @@ -{"version":1,"defects":{"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordMD5":4},"times":{"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordBcrypt":0.004,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordFirebase":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordPbkdf2":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordDjango":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordMD5":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordWithCleartext":0,"Descope\\Tests\\Management\\UserPwdTest::testUserPasswordWithHashedPassword":0}} \ No newline at end of file diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 2bf29c2..4a48677 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -13,7 +13,6 @@ use Descope\SDK\Auth\Management\Audit; use Descope\SDK\EndpointsV1; use Descope\SDK\EndpointsV2; -use Descope\SDK\Exception\AuthException; use Descope\SDK\Management\MgmtV1; @@ -63,10 +62,10 @@ public function __construct(array $config) */ public function verify($sessionToken = null): bool { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; if (!$sessionToken) { - throw new \InvalidArgumentException('Session token cannot be null or empty.'); + throw new \InvalidArgumentException('Session token is required.'); } $verifier = new Verifier($this->config, $this->api); @@ -82,10 +81,10 @@ public function verify($sessionToken = null): bool */ public function refreshSession($refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (empty($refreshToken)) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw new AuthException('Refresh token cannot be null or empty.'); } try { @@ -112,11 +111,11 @@ public function refreshSession($refreshToken = null): array */ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null): array { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (empty($sessionToken) || empty($refreshToken)) { - throw new \InvalidArgumentException('Session or refresh token cannot be null or empty.'); + throw new AuthException(400, 'Session or refresh token cannot be null or empty.'); } try { @@ -136,10 +135,10 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu */ public function getClaims($token = null): array { - $token = $token ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; + $token = $token ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; if (!$token) { - throw new \InvalidArgumentException('Session token cannot be null or empty.'); + throw new \InvalidArgumentException('Token is required.'); } $extractor = new Extractor($this->config); @@ -155,10 +154,10 @@ public function getClaims($token = null): array */ public function getUserDetails(string $refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw new \InvalidArgumentException('Refresh token is required.'); } try { @@ -183,10 +182,10 @@ public function getUserDetails(string $refreshToken = null): array */ public function logout(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw new \InvalidArgumentException('Refresh token is required.'); } try { @@ -213,10 +212,10 @@ public function logout(string $refreshToken = null): void */ public function logoutAll(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw new \InvalidArgumentException('Refresh token is required.'); } try { diff --git a/src/SDK/Management/Password/UserPasswordMD5.php b/src/SDK/Management/Password/UserPasswordMD5.php deleted file mode 100644 index a83ca3e..0000000 --- a/src/SDK/Management/Password/UserPasswordMD5.php +++ /dev/null @@ -1,41 +0,0 @@ -hash = $hash; - } - - /** - * Convert object data to an array format. - * - * @return array The password data as an associative array. - */ - public function toArray(): array - { - return [ - 'md5' => [ - 'hash' => $this->hash, - ], - ]; - } -} \ No newline at end of file diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php index 5e822be..ef074ba 100644 --- a/src/SDK/Management/Role.php +++ b/src/SDK/Management/Role.php @@ -29,10 +29,8 @@ public function __construct(API $api) * @return bool True if tenant permissions are valid, false otherwise. * @throws AuthException If JWT response is invalid. */ - public function validateTenantPermissions(array $jwtResponse, array $permissions, ?string $tenant = null): bool + public function validateTenantPermissions(array $jwtResponse, string $tenant = '', array $permissions): bool { - $tenant = $tenant ?? ''; - if (!is_array($permissions)) { $permissions = [$permissions]; } diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index ad76a74..3dc3edb 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -67,7 +67,7 @@ public function parseToken(string $sessionToken): array /** * Validate a JWT using the provided JWK Set. - */ + */ public function validateJWT(string $sessionToken): array { $useRefreshedKey = false; diff --git a/src/tests/DescopeSDKTest.php b/src/tests/DescopeSDKTest.php index 22e0d85..37ebfbb 100644 --- a/src/tests/DescopeSDKTest.php +++ b/src/tests/DescopeSDKTest.php @@ -1,15 +1,11 @@ assertInstanceOf(Password::class, $this->sdk->password()); - $this->assertInstanceOf(SSO::class, $this->sdk->sso()); - $this->assertInstanceOf(Management::class, $this->sdk->management()); + $this->assertInstanceOf(SDKConfig::class, $this->sdk->password()); + $this->assertInstanceOf(SDKConfig::class, $this->sdk->sso()); + $this->assertInstanceOf(SDKConfig::class, $this->sdk->management()); } public function testVerifyThrowsExceptionWithoutToken() diff --git a/src/tests/Management/UserPwdTest.php b/src/tests/Management/UserPwdTest.php index aa3ace7..ec6eeb6 100644 --- a/src/tests/Management/UserPwdTest.php +++ b/src/tests/Management/UserPwdTest.php @@ -8,7 +8,6 @@ use Descope\SDK\Management\Password\UserPasswordFirebase; use Descope\SDK\Management\Password\UserPasswordPbkdf2; use Descope\SDK\Management\Password\UserPasswordDjango; -use Descope\SDK\Management\Password\UserPasswordMD5; class UserPwdTest extends TestCase { @@ -82,19 +81,6 @@ public function testUserPasswordDjango() $this->assertEquals($expectedArray, $userPasswordDjango->toArray()); } - public function testUserPasswordMD5() - { - $md5Hash = 'pbkdf2_sha256$30000$hashvalue'; - $userPasswordMD5 = new UserPasswordMD5($md5Hash); - $expectedArray = [ - 'md5' => [ - 'hash' => $md5Hash, - ], - ]; - - $this->assertEquals($expectedArray, $userPasswordMD5->toArray()); - } - public function testUserPasswordWithCleartext() { $cleartextPassword = 'mypassword'; diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index 0ba158f..19ecf65 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -6,6 +6,9 @@ use Descope\SDK\DescopeSDK; use Descope\SDK\Management\Password\UserPassword; use Descope\SDK\Management\Password\UserPasswordBcrypt; +use Descope\SDK\Management\Password\UserPasswordFirebase; +use Descope\SDK\Management\Password\UserPasswordPbkdf2; +use Descope\SDK\Management\Password\UserPasswordDjango; use Descope\SDK\Management\User; use Descope\SDK\Management\AssociatedTenant; use Descope\SDK\Management\UserObj; From e2ea284284cdb5bc4a0c734a704a250356471e3b Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:17:02 -0800 Subject: [PATCH 09/21] added MD5 support --- src/SDK/Configuration/SDKConfig.php | 7 ++++ src/SDK/DescopeSDK.php | 31 +++++++------- .../Management/Password/UserPasswordMD5.php | 41 +++++++++++++++++++ src/SDK/Token/Extractor.php | 2 +- src/tests/Management/UserPwdTest.php | 14 +++++++ 5 files changed, 79 insertions(+), 16 deletions(-) create mode 100644 src/SDK/Management/Password/UserPasswordMD5.php diff --git a/src/SDK/Configuration/SDKConfig.php b/src/SDK/Configuration/SDKConfig.php index 4877187..67396ea 100644 --- a/src/SDK/Configuration/SDKConfig.php +++ b/src/SDK/Configuration/SDKConfig.php @@ -47,6 +47,7 @@ private function fetchJWKSets(): array { try { $url = EndpointsV2::getPublicKeyPath() . '/' . $this->projectId; +<<<<<<< Updated upstream $response = $this->client->request('GET', $url); $jwkSets = json_decode($response->getBody(), true); @@ -54,6 +55,12 @@ private function fetchJWKSets(): array throw new \Exception('Invalid JWK response'); } +======= + + // Fetch JWK public key from Descope API + $res = $this->client->request('GET', $url); + $jwkSets = json_decode($res->getBody(), true); +>>>>>>> Stashed changes return $jwkSets; } catch (RequestException $e) { throw new \Exception('Failed to fetch JWK KeySet: ' . $e->getMessage()); diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 4a48677..2bf29c2 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -13,6 +13,7 @@ use Descope\SDK\Auth\Management\Audit; use Descope\SDK\EndpointsV1; use Descope\SDK\EndpointsV2; +use Descope\SDK\Exception\AuthException; use Descope\SDK\Management\MgmtV1; @@ -62,10 +63,10 @@ public function __construct(array $config) */ public function verify($sessionToken = null): bool { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$sessionToken) { - throw new \InvalidArgumentException('Session token is required.'); + throw new \InvalidArgumentException('Session token cannot be null or empty.'); } $verifier = new Verifier($this->config, $this->api); @@ -81,10 +82,10 @@ public function verify($sessionToken = null): bool */ public function refreshSession($refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (empty($refreshToken)) { - throw new AuthException('Refresh token cannot be null or empty.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { @@ -111,11 +112,11 @@ public function refreshSession($refreshToken = null): array */ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = null): array { - $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (empty($sessionToken) || empty($refreshToken)) { - throw new AuthException(400, 'Session or refresh token cannot be null or empty.'); + throw new \InvalidArgumentException('Session or refresh token cannot be null or empty.'); } try { @@ -135,10 +136,10 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu */ public function getClaims($token = null): array { - $token = $token ?? $_COOKIE[EndpointsV1::SESSION_COOKIE_NAME] ?? null; + $token = $token ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$token) { - throw new \InvalidArgumentException('Token is required.'); + throw new \InvalidArgumentException('Session token cannot be null or empty.'); } $extractor = new Extractor($this->config); @@ -154,10 +155,10 @@ public function getClaims($token = null): array */ public function getUserDetails(string $refreshToken = null): array { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { @@ -182,10 +183,10 @@ public function getUserDetails(string $refreshToken = null): array */ public function logout(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { @@ -212,10 +213,10 @@ public function logout(string $refreshToken = null): void */ public function logoutAll(string $refreshToken = null): void { - $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::REFRESH_COOKIE_NAME] ?? null; + $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token is required.'); + throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); } try { diff --git a/src/SDK/Management/Password/UserPasswordMD5.php b/src/SDK/Management/Password/UserPasswordMD5.php new file mode 100644 index 0000000..a83ca3e --- /dev/null +++ b/src/SDK/Management/Password/UserPasswordMD5.php @@ -0,0 +1,41 @@ +hash = $hash; + } + + /** + * Convert object data to an array format. + * + * @return array The password data as an associative array. + */ + public function toArray(): array + { + return [ + 'md5' => [ + 'hash' => $this->hash, + ], + ]; + } +} \ No newline at end of file diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index 3dc3edb..ad76a74 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -67,7 +67,7 @@ public function parseToken(string $sessionToken): array /** * Validate a JWT using the provided JWK Set. - */ + */ public function validateJWT(string $sessionToken): array { $useRefreshedKey = false; diff --git a/src/tests/Management/UserPwdTest.php b/src/tests/Management/UserPwdTest.php index ec6eeb6..aa3ace7 100644 --- a/src/tests/Management/UserPwdTest.php +++ b/src/tests/Management/UserPwdTest.php @@ -8,6 +8,7 @@ use Descope\SDK\Management\Password\UserPasswordFirebase; use Descope\SDK\Management\Password\UserPasswordPbkdf2; use Descope\SDK\Management\Password\UserPasswordDjango; +use Descope\SDK\Management\Password\UserPasswordMD5; class UserPwdTest extends TestCase { @@ -81,6 +82,19 @@ public function testUserPasswordDjango() $this->assertEquals($expectedArray, $userPasswordDjango->toArray()); } + public function testUserPasswordMD5() + { + $md5Hash = 'pbkdf2_sha256$30000$hashvalue'; + $userPasswordMD5 = new UserPasswordMD5($md5Hash); + $expectedArray = [ + 'md5' => [ + 'hash' => $md5Hash, + ], + ]; + + $this->assertEquals($expectedArray, $userPasswordMD5->toArray()); + } + public function testUserPasswordWithCleartext() { $cleartextPassword = 'mypassword'; From 1ac0689037a98a3f86091af395e8a6059e97420c Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:18:12 -0800 Subject: [PATCH 10/21] cleaned up code --- src/SDK/Management/Role.php | 4 +++- src/tests/DescopeSDKTest.php | 14 +++++++++----- src/tests/Management/UserTest.php | 3 --- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/SDK/Management/Role.php b/src/SDK/Management/Role.php index ef074ba..5e822be 100644 --- a/src/SDK/Management/Role.php +++ b/src/SDK/Management/Role.php @@ -29,8 +29,10 @@ public function __construct(API $api) * @return bool True if tenant permissions are valid, false otherwise. * @throws AuthException If JWT response is invalid. */ - public function validateTenantPermissions(array $jwtResponse, string $tenant = '', array $permissions): bool + public function validateTenantPermissions(array $jwtResponse, array $permissions, ?string $tenant = null): bool { + $tenant = $tenant ?? ''; + if (!is_array($permissions)) { $permissions = [$permissions]; } diff --git a/src/tests/DescopeSDKTest.php b/src/tests/DescopeSDKTest.php index 37ebfbb..22e0d85 100644 --- a/src/tests/DescopeSDKTest.php +++ b/src/tests/DescopeSDKTest.php @@ -1,11 +1,15 @@ assertInstanceOf(SDKConfig::class, $this->sdk->password()); - $this->assertInstanceOf(SDKConfig::class, $this->sdk->sso()); - $this->assertInstanceOf(SDKConfig::class, $this->sdk->management()); + $this->assertInstanceOf(Password::class, $this->sdk->password()); + $this->assertInstanceOf(SSO::class, $this->sdk->sso()); + $this->assertInstanceOf(Management::class, $this->sdk->management()); } public function testVerifyThrowsExceptionWithoutToken() diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index 19ecf65..0ba158f 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -6,9 +6,6 @@ use Descope\SDK\DescopeSDK; use Descope\SDK\Management\Password\UserPassword; use Descope\SDK\Management\Password\UserPasswordBcrypt; -use Descope\SDK\Management\Password\UserPasswordFirebase; -use Descope\SDK\Management\Password\UserPasswordPbkdf2; -use Descope\SDK\Management\Password\UserPasswordDjango; use Descope\SDK\Management\User; use Descope\SDK\Management\AssociatedTenant; use Descope\SDK\Management\UserObj; From e1da1b5f1d1a57bceea81eadfd01acadee340258 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:22:14 -0800 Subject: [PATCH 11/21] added gha action --- .github/workflows/phpunit.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 445a05b..551e3f7 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -9,6 +9,9 @@ jobs: test: runs-on: ubuntu-latest + env: + DESCOPE_PROJECT_ID: 'P2OkfVnJi5Ht7mpCqHjx17nV5epH' + steps: - uses: actions/checkout@v2 From 831921211042299c8d5aa96449abb628bd691c32 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:25:22 -0800 Subject: [PATCH 12/21] added phpunit.yml --- .github/workflows/phpunit.yml | 2 +- src/SDK/Configuration/SDKConfig.php | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 551e3f7..f61ebc3 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest env: - DESCOPE_PROJECT_ID: 'P2OkfVnJi5Ht7mpCqHjx17nV5epH' + DESCOPE_PROJECT_ID: ${{ vars.DESCOPE_PROJECT_ID }} steps: - uses: actions/checkout@v2 diff --git a/src/SDK/Configuration/SDKConfig.php b/src/SDK/Configuration/SDKConfig.php index 67396ea..4877187 100644 --- a/src/SDK/Configuration/SDKConfig.php +++ b/src/SDK/Configuration/SDKConfig.php @@ -47,7 +47,6 @@ private function fetchJWKSets(): array { try { $url = EndpointsV2::getPublicKeyPath() . '/' . $this->projectId; -<<<<<<< Updated upstream $response = $this->client->request('GET', $url); $jwkSets = json_decode($response->getBody(), true); @@ -55,12 +54,6 @@ private function fetchJWKSets(): array throw new \Exception('Invalid JWK response'); } -======= - - // Fetch JWK public key from Descope API - $res = $this->client->request('GET', $url); - $jwkSets = json_decode($res->getBody(), true); ->>>>>>> Stashed changes return $jwkSets; } catch (RequestException $e) { throw new \Exception('Failed to fetch JWK KeySet: ' . $e->getMessage()); From 1c611c15884588867d50d44a570dd80d24ab7f26 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Fri, 27 Dec 2024 15:27:06 -0800 Subject: [PATCH 13/21] fixed linter --- src/SDK/Token/Extractor.php | 8 +------- src/SDK/Token/Verifier.php | 1 - 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index ad76a74..4afee07 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -104,7 +104,6 @@ public function validateJWT(string $sessionToken): array } return $jwt['payload']; - } catch (TokenException $e) { if ($useRefreshedKey) { throw new TokenException('JWT validation failed after retry: ' . $e->getMessage()); @@ -159,12 +158,7 @@ private function convertJWKToPEM(array $jwk): string $modulus = pack('Ca*a*', 0x02, $this->encodeLength(strlen($modulus)), $modulus); $exponent = pack('Ca*a*', 0x02, $this->encodeLength(strlen($exponent)), $exponent); - $rsaPublicKey = pack( - 'Ca*a*', - 0x30, - $this->encodeLength(strlen($modulus . $exponent)), - $modulus . $exponent - ); + $rsaPublicKey = pack('Ca*a*', 0x30, $this->encodeLength(strlen($modulus . $exponent)), $modulus . $exponent); // Add RSA public key algorithm identifier $algorithmIdentifier = pack('H*', '300d06092a864886f70d0101010500'); diff --git a/src/SDK/Token/Verifier.php b/src/SDK/Token/Verifier.php index a5e07e7..bafb996 100644 --- a/src/SDK/Token/Verifier.php +++ b/src/SDK/Token/Verifier.php @@ -70,7 +70,6 @@ public function verify(string $sessionToken, ?string $audience = null): bool // All validations passed return true; - } catch (TokenException $e) { // You might want to throw a specific error or return false depending on your needs throw new TokenException('Token validation failed: ' . $e->getMessage()); From ba29d1fae468c592fd06fbe452000a5278fc2e6a Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Sun, 29 Dec 2024 20:54:55 -0800 Subject: [PATCH 14/21] added fix for tests --- phpunit.xml | 2 +- src/tests/Auth/PasswordTest.php | 2 +- src/tests/Auth/SSOTest.php | 1 + src/tests/Management/AuditTest.php | 4 ++-- src/tests/Management/UserTest.php | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index ef67d5b..6520de3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -2,7 +2,7 @@ - src/tests + src/tests/DescopeSDKTest.php \ No newline at end of file diff --git a/src/tests/Auth/PasswordTest.php b/src/tests/Auth/PasswordTest.php index 504796b..f279101 100644 --- a/src/tests/Auth/PasswordTest.php +++ b/src/tests/Auth/PasswordTest.php @@ -17,7 +17,7 @@ protected function setUp(): void { $this->apiMock = $this->createMock(API::class); $this->password = new Password($this->apiMock); - EndpointsV1::setBaseUrl($_ENV['DESCOPE_PROJECT_ID']); + EndpointsV1::setBaseUrl('descope_project_id'); } public function testSignUp() diff --git a/src/tests/Auth/SSOTest.php b/src/tests/Auth/SSOTest.php index 0f549ff..b2ca915 100644 --- a/src/tests/Auth/SSOTest.php +++ b/src/tests/Auth/SSOTest.php @@ -18,6 +18,7 @@ protected function setUp(): void { $this->apiMock = $this->createMock(API::class); $this->sso = new SSO($this->apiMock); + EndpointsV1::setBaseUrl('descope_project_id'); } public function testSSOSignIn(): void diff --git a/src/tests/Management/AuditTest.php b/src/tests/Management/AuditTest.php index 75b310c..1b5f9c1 100644 --- a/src/tests/Management/AuditTest.php +++ b/src/tests/Management/AuditTest.php @@ -12,8 +12,8 @@ class AuditTest extends TestCase protected function setUp(): void { $config = [ - 'projectId' => 'P2OkfVnJi5Ht7mpCqHjx17nV5epH', - 'managementKey' => 'K2o2rLwk3N3QI7kyJcRUmULKXqB7mKzpY7Dk6Hl24IXRM25YcYDYPFMKCO4SmUTDJJluxlu', + 'projectId' => 'descope_project_id', + 'managementKey' => 'descope_management_key', ]; $this->descopeSDK = new DescopeSDK($config); diff --git a/src/tests/Management/UserTest.php b/src/tests/Management/UserTest.php index 0ba158f..5d98fb9 100644 --- a/src/tests/Management/UserTest.php +++ b/src/tests/Management/UserTest.php @@ -23,8 +23,8 @@ class UserTest extends TestCase protected function setUp(): void { $config = [ - 'projectId' => 'YOUR_PROJECT_ID', - 'managementKey' => 'YOUR_MANAGEMENT_KEY', + 'projectId' => 'descope_project_id', + 'managementKey' => 'descope_management_key', ]; $this->descopeSDK = new DescopeSDK($config); From 6826d7674ba9dfb8e0f2018e9759939d6e5aa602 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Thu, 2 Jan 2025 08:45:29 -0800 Subject: [PATCH 15/21] Update src/SDK/Configuration/SDKConfig.php Co-authored-by: Omer Cohen --- src/SDK/Configuration/SDKConfig.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SDK/Configuration/SDKConfig.php b/src/SDK/Configuration/SDKConfig.php index 4877187..e1c9b0d 100644 --- a/src/SDK/Configuration/SDKConfig.php +++ b/src/SDK/Configuration/SDKConfig.php @@ -26,7 +26,7 @@ public function __construct(array $config) } /** - * Gets the current JWK KeySet. Fetches a new one if not cached or if explicitly requested. + * Gets the current JWKSet. Fetches a new one if not cached or if explicitly requested. */ public function getJWKSets(bool $forceRefresh = false): array { From 393e99265113935c614d17ee3dbc60b2330f2b47 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Thu, 2 Jan 2025 08:54:31 -0800 Subject: [PATCH 16/21] added new lines at bottom of Password classes --- src/SDK/Management/Password/UserPasswordBcrypt.php | 2 +- src/SDK/Management/Password/UserPasswordDjango.php | 2 +- src/SDK/Management/Password/UserPasswordFirebase.php | 2 +- src/SDK/Management/Password/UserPasswordMD5.php | 2 +- src/SDK/Management/Password/UserPasswordPHPass.php | 2 +- src/SDK/Management/Password/UserPasswordPbkdf2.php | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SDK/Management/Password/UserPasswordBcrypt.php b/src/SDK/Management/Password/UserPasswordBcrypt.php index c166168..bdcb6d1 100644 --- a/src/SDK/Management/Password/UserPasswordBcrypt.php +++ b/src/SDK/Management/Password/UserPasswordBcrypt.php @@ -37,4 +37,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} diff --git a/src/SDK/Management/Password/UserPasswordDjango.php b/src/SDK/Management/Password/UserPasswordDjango.php index 4d2528c..58bd92c 100644 --- a/src/SDK/Management/Password/UserPasswordDjango.php +++ b/src/SDK/Management/Password/UserPasswordDjango.php @@ -37,4 +37,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} diff --git a/src/SDK/Management/Password/UserPasswordFirebase.php b/src/SDK/Management/Password/UserPasswordFirebase.php index 2ee4339..f46cd3b 100644 --- a/src/SDK/Management/Password/UserPasswordFirebase.php +++ b/src/SDK/Management/Password/UserPasswordFirebase.php @@ -63,4 +63,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} diff --git a/src/SDK/Management/Password/UserPasswordMD5.php b/src/SDK/Management/Password/UserPasswordMD5.php index a83ca3e..a2b5130 100644 --- a/src/SDK/Management/Password/UserPasswordMD5.php +++ b/src/SDK/Management/Password/UserPasswordMD5.php @@ -38,4 +38,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} diff --git a/src/SDK/Management/Password/UserPasswordPHPass.php b/src/SDK/Management/Password/UserPasswordPHPass.php index 33a4257..ceb1098 100644 --- a/src/SDK/Management/Password/UserPasswordPHPass.php +++ b/src/SDK/Management/Password/UserPasswordPHPass.php @@ -49,4 +49,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} diff --git a/src/SDK/Management/Password/UserPasswordPbkdf2.php b/src/SDK/Management/Password/UserPasswordPbkdf2.php index 1de37dd..cec8bbb 100644 --- a/src/SDK/Management/Password/UserPasswordPbkdf2.php +++ b/src/SDK/Management/Password/UserPasswordPbkdf2.php @@ -53,4 +53,4 @@ public function toArray(): array ], ]; } -} \ No newline at end of file +} From 134de0e0db45a91f29d8f9572c188ae6d19b588a Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:57:21 -0800 Subject: [PATCH 17/21] added matrix strategy for phpunit tests --- .github/workflows/phpunit.yml | 6 +++++- sample/callback.php | 2 +- sample/login.php | 2 +- src/tests/Management/UserPwdTest.php | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index f61ebc3..77264d5 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -12,13 +12,17 @@ jobs: env: DESCOPE_PROJECT_ID: ${{ vars.DESCOPE_PROJECT_ID }} + strategy: + matrix: + php-version: [8.1, 7.3] + steps: - uses: actions/checkout@v2 - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.0' + php-version: ${{ matrix.php-version }} - name: Install dependencies run: composer install diff --git a/sample/callback.php b/sample/callback.php index 3c9f0d1..7e1da49 100644 --- a/sample/callback.php +++ b/sample/callback.php @@ -35,7 +35,7 @@ error_log("Session token verification failed."); $descopeSDK->logout(); // Redirect to login page - // header('Location: login.php'); + header('Location: login.php'); exit(); } } else { diff --git a/sample/login.php b/sample/login.php index 7c0d070..8e37477 100644 --- a/sample/login.php +++ b/sample/login.php @@ -76,7 +76,7 @@ function sendFormData(sessionToken, userDetails) { sdk.logout(); console.log("No valid refresh token. Displaying login form."); const container = document.getElementById("container") - container.innerHTML = ''; + container.innerHTML = ''; const wcElement = document.getElementsByTagName('descope-wc')[0]; const onSuccess = async (e) => { diff --git a/src/tests/Management/UserPwdTest.php b/src/tests/Management/UserPwdTest.php index aa3ace7..c5832b9 100644 --- a/src/tests/Management/UserPwdTest.php +++ b/src/tests/Management/UserPwdTest.php @@ -84,7 +84,7 @@ public function testUserPasswordDjango() public function testUserPasswordMD5() { - $md5Hash = 'pbkdf2_sha256$30000$hashvalue'; + $md5Hash = '87f77988ccb5aa917c93201ba314fcd4'; $userPasswordMD5 = new UserPasswordMD5($md5Hash); $expectedArray = [ 'md5' => [ From a117f3276d72de9232b47ddeb3a8b8c89b08269c Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:10:20 -0800 Subject: [PATCH 18/21] added TokenValidator class --- .github/workflows/phpunit.yml | 2 +- composer.lock | 92 ++++++++++++------------- src/SDK/DescopeSDK.php | 15 ++-- src/SDK/Exception/ValidateException.php | 57 +++++++++++++++ 4 files changed, 112 insertions(+), 54 deletions(-) create mode 100644 src/SDK/Exception/ValidateException.php diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 77264d5..101487b 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - php-version: [8.1, 7.3] + php-version: [8.1, 7.4] steps: - uses: actions/checkout@v2 diff --git a/composer.lock b/composer.lock index 46212a7..9f6cc75 100644 --- a/composer.lock +++ b/composer.lock @@ -196,16 +196,16 @@ }, { "name": "guzzlehttp/promises", - "version": "2.0.3", + "version": "2.0.4", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8" + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", - "reference": "6ea8dd08867a2a42619d65c3deb2c0fcbf81c8f8", + "url": "https://api.github.com/repos/guzzle/promises/zipball/f9c436286ab2892c7db7be8c8da4ef61ccf7b455", + "reference": "f9c436286ab2892c7db7be8c8da4ef61ccf7b455", "shasum": "" }, "require": { @@ -259,7 +259,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.0.3" + "source": "https://github.com/guzzle/promises/tree/2.0.4" }, "funding": [ { @@ -275,7 +275,7 @@ "type": "tidelift" } ], - "time": "2024-07-18T10:29:17+00:00" + "time": "2024-10-17T10:06:22+00:00" }, { "name": "guzzlehttp/psr7", @@ -741,16 +741,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v2.5.3", + "version": "v2.5.4", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d" + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/80d075412b557d41002320b96a096ca65aa2c98d", - "reference": "80d075412b557d41002320b96a096ca65aa2c98d", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", "shasum": "" }, "require": { @@ -758,12 +758,12 @@ }, "type": "library", "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, "branch-alias": { "dev-main": "2.5-dev" - }, - "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -788,7 +788,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.3" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" }, "funding": [ { @@ -804,7 +804,7 @@ "type": "tidelift" } ], - "time": "2023-01-24T14:02:46+00:00" + "time": "2024-09-25T14:11:13+00:00" }, { "name": "symfony/polyfill-ctype", @@ -832,8 +832,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -911,8 +911,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -985,8 +985,8 @@ "type": "library", "extra": { "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" } }, "autoload": { @@ -1203,16 +1203,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.12.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", - "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", + "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", "shasum": "" }, "require": { @@ -1251,7 +1251,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" }, "funding": [ { @@ -1259,20 +1259,20 @@ "type": "tidelift" } ], - "time": "2024-06-12T14:39:25+00:00" + "time": "2024-11-08T17:47:46+00:00" }, { "name": "nikic/php-parser", - "version": "v5.2.0", + "version": "v5.4.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb" + "reference": "447a020a1f875a434d62f2a401f53b82a396e494" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", - "reference": "23c79fbbfb725fb92af9bcf41065c8e9a0d49ddb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", + "reference": "447a020a1f875a434d62f2a401f53b82a396e494", "shasum": "" }, "require": { @@ -1315,9 +1315,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.2.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" }, - "time": "2024-09-15T16:40:33+00:00" + "time": "2024-12-30T11:07:19+00:00" }, { "name": "phar-io/manifest", @@ -1758,16 +1758,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.21", + "version": "9.6.22", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa" + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", - "reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "shasum": "" }, "require": { @@ -1778,7 +1778,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.0", + "myclabs/deep-copy": "^1.12.1", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -1841,7 +1841,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" }, "funding": [ { @@ -1857,7 +1857,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T10:50:18+00:00" + "time": "2024-12-05T13:48:26+00:00" }, { "name": "sebastian/cli-parser", @@ -2824,16 +2824,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.10.3", + "version": "3.11.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c" + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/62d32998e820bddc40f99f8251958aed187a5c9c", - "reference": "62d32998e820bddc40f99f8251958aed187a5c9c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/1368f4a58c3c52114b86b1abe8f4098869cb0079", + "reference": "1368f4a58c3c52114b86b1abe8f4098869cb0079", "shasum": "" }, "require": { @@ -2900,7 +2900,7 @@ "type": "open_collective" } ], - "time": "2024-09-18T10:38:58+00:00" + "time": "2024-12-11T16:04:26+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 2bf29c2..4e028a3 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -14,6 +14,7 @@ use Descope\SDK\EndpointsV1; use Descope\SDK\EndpointsV2; use Descope\SDK\Exception\AuthException; +use Descope\SDK\Exception\ValidateException; use Descope\SDK\Management\MgmtV1; @@ -66,7 +67,7 @@ public function verify($sessionToken = null): bool $sessionToken = $sessionToken ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$sessionToken) { - throw new \InvalidArgumentException('Session token cannot be null or empty.'); + throw ValidationException::forMissingSessionToken(); } $verifier = new Verifier($this->config, $this->api); @@ -85,7 +86,7 @@ public function refreshSession($refreshToken = null): array $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (empty($refreshToken)) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw ValidationException::forMissingRefreshToken(); } try { @@ -116,7 +117,7 @@ public function verifyAndRefreshSession($sessionToken = null, $refreshToken = nu $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (empty($sessionToken) || empty($refreshToken)) { - throw new \InvalidArgumentException('Session or refresh token cannot be null or empty.'); + throw new ValidateException('Session or refresh token cannot be null or empty.'); } try { @@ -139,7 +140,7 @@ public function getClaims($token = null): array $token = $token ?? $_COOKIE[EndpointsV1::$SESSION_COOKIE_NAME] ?? null; if (!$token) { - throw new \InvalidArgumentException('Session token cannot be null or empty.'); + throw ValidationException::forMissingSessionToken(); } $extractor = new Extractor($this->config); @@ -158,7 +159,7 @@ public function getUserDetails(string $refreshToken = null): array $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw ValidationException::forMissingRefreshToken(); } try { @@ -186,7 +187,7 @@ public function logout(string $refreshToken = null): void $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw ValidationException::forMissingRefreshToken(); } try { @@ -216,7 +217,7 @@ public function logoutAll(string $refreshToken = null): void $refreshToken = $refreshToken ?? $_COOKIE[EndpointsV1::$REFRESH_COOKIE_NAME] ?? null; if (!$refreshToken) { - throw new \InvalidArgumentException('Refresh token cannot be null or empty.'); + throw ValidationException::forMissingRefreshToken(); } try { diff --git a/src/SDK/Exception/ValidateException.php b/src/SDK/Exception/ValidateException.php new file mode 100644 index 0000000..a5fa4ae --- /dev/null +++ b/src/SDK/Exception/ValidateException.php @@ -0,0 +1,57 @@ + Date: Thu, 2 Jan 2025 10:17:29 -0800 Subject: [PATCH 19/21] fixed name --- src/SDK/DescopeSDK.php | 2 +- .../{ValidateException.php => ValidationException.php} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/SDK/Exception/{ValidateException.php => ValidationException.php} (100%) diff --git a/src/SDK/DescopeSDK.php b/src/SDK/DescopeSDK.php index 4e028a3..554ab77 100644 --- a/src/SDK/DescopeSDK.php +++ b/src/SDK/DescopeSDK.php @@ -14,7 +14,7 @@ use Descope\SDK\EndpointsV1; use Descope\SDK\EndpointsV2; use Descope\SDK\Exception\AuthException; -use Descope\SDK\Exception\ValidateException; +use Descope\SDK\Exception\ValidationException; use Descope\SDK\Management\MgmtV1; diff --git a/src/SDK/Exception/ValidateException.php b/src/SDK/Exception/ValidationException.php similarity index 100% rename from src/SDK/Exception/ValidateException.php rename to src/SDK/Exception/ValidationException.php From 1dedcbaeaf11aefec94920079a8cd1011b2b52e9 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:33:11 -0800 Subject: [PATCH 20/21] added abstract class for caching --- README.md | 56 ++++++++++++++++++++++++++++- src/SDK/Cache/APCuCache.php | 21 +++++++++++ src/SDK/Cache/CacheInterface.php | 10 ++++++ src/SDK/Cache/LaravelCache.php | 23 ++++++++++++ src/SDK/Cache/NullCache.php | 21 +++++++++++ src/SDK/Configuration/SDKConfig.php | 32 ++++++++++++----- src/SDK/Token/Extractor.php | 20 +++++------ src/tests/DescopeSDKTest.php | 5 +-- 8 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 src/SDK/Cache/APCuCache.php create mode 100644 src/SDK/Cache/CacheInterface.php create mode 100644 src/SDK/Cache/LaravelCache.php create mode 100644 src/SDK/Cache/NullCache.php diff --git a/README.md b/README.md index 3778e3b..a9ebc22 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,61 @@ $descopeSDK = new DescopeSDK([ ]); ``` -This SDK will easily allow you to handle Descope JWT tokens with the following built-in functions: +### Caching Mechanism + +The Descope PHP SDK uses a caching mechanism to store frequently accessed data, such as JSON Web Key Sets (JWKs) for session token validation. By default, the SDK uses **APCu** for caching, provided it is enabled and configured in your environment. If APCu is not available, and no other caching mechanism is provided, caching is disabled. + +By using the `CacheInterface`, you can integrate the Descope PHP SDK with any caching mechanism that suits your application, ensuring optimal performance in both small and large-scale deployments. + +#### Custom Caching with `CacheInterface` + +The SDK allows you to provide a custom caching mechanism by implementing the `CacheInterface`. This interface defines three methods that any cache implementation should support: + +- `get(string $key)`: Retrieve a value by key. +- `set(string $key, $value, int $ttl = 3600): bool`: Store a value with a specified time-to-live (TTL). +- `delete(string $key): bool`: Remove a value by key. + +You can provide your custom caching implementation by creating a class that implements `CacheInterface`. Here’s an example using Laravel’s cache system: + +```php +namespace App\Cache; + +use Descope\SDK\Cache\CacheInterface; +use Illuminate\Support\Facades\Cache; + +class LaravelCache implements CacheInterface +{ + public function get(string $key) + { + return Cache::get($key); + } + + public function set(string $key, $value, int $ttl = 3600): bool + { + // Laravel TTL is in minutes + return Cache::put($key, $value, $ttl / 60); + } + + public function delete(string $key): bool + { + return Cache::forget($key); + } +} +``` + +To use the Laravel cache in the SDK: + +```php +use Descope\SDK\DescopeSDK; +use App\Cache\LaravelCache; + +$descopeSDK = new DescopeSDK([ + 'projectId' => $_ENV['DESCOPE_PROJECT_ID'], + 'managementKey' => $_ENV['DESCOPE_MANAGEMENT_KEY'], +], new LaravelCache()); +``` + +Once you've configured your caching, you're ready to use the SDK. This SDK will easily allow you integrate Descope functionality with the following built-in functions: ## Password Authentication diff --git a/src/SDK/Cache/APCuCache.php b/src/SDK/Cache/APCuCache.php new file mode 100644 index 0000000..17e3d1a --- /dev/null +++ b/src/SDK/Cache/APCuCache.php @@ -0,0 +1,21 @@ +client = new Client(); $this->projectId = $config['projectId']; $this->managementKey = $config['managementKey'] ?? ''; + if ($cache) { + $this->cache = $cache; + } elseif (extension_loaded('apcu') && ini_get('apc.enable_cli')) { + $this->cache = new APCuCache(); + } else { + $this->cache = new NullCache(); + error_log('APCu is not enabled. Falling back to NullCache. Caching is disabled.'); + } + EndpointsV2::setBaseUrl($config['projectId']); } @@ -30,14 +42,16 @@ public function __construct(array $config) */ public function getJWKSets(bool $forceRefresh = false): array { - // Return cached JWK if it exists and no refresh is requested - if ($this->cachedJWKSets !== null && !$forceRefresh) { - return $this->cachedJWKSets; + if (!$forceRefresh) { + $cachedJWKSets = $this->cache->get(self::JWKS_CACHE_KEY); + if ($cachedJWKSets) { + return $cachedJWKSets; + } } - // Fetch new JWK KeySet from Descope API - $this->cachedJWKSets = $this->fetchJWKSets(); - return $this->cachedJWKSets; + $jwks = $this->fetchJWKSets(); + $this->cache->set(self::JWKS_CACHE_KEY, $jwks, 3600); // Cache for 1 hour + return $jwks; } /** diff --git a/src/SDK/Token/Extractor.php b/src/SDK/Token/Extractor.php index 4afee07..4374f7d 100644 --- a/src/SDK/Token/Extractor.php +++ b/src/SDK/Token/Extractor.php @@ -125,18 +125,14 @@ private function verifySignature(string $signedData, string $signature, string $ throw new TokenException('Invalid public key'); } - try { - $result = openssl_verify( - $signedData, - $signature, - $publicKey, - OPENSSL_ALGO_SHA256 - ); - - return $result === 1; - } finally { - openssl_free_key($publicKey); - } + $result = openssl_verify( + $signedData, + $signature, + $publicKey, + OPENSSL_ALGO_SHA256 + ); + + return $result === 1; } /** diff --git a/src/tests/DescopeSDKTest.php b/src/tests/DescopeSDKTest.php index 22e0d85..9b8d7c7 100644 --- a/src/tests/DescopeSDKTest.php +++ b/src/tests/DescopeSDKTest.php @@ -8,6 +8,7 @@ use Descope\SDK\Auth\Password; use Descope\SDK\Auth\SSO; use Descope\SDK\Management\Management; +use Descope\SDK\Exception\ValidationException; final class DescopeSDKTest extends TestCase { @@ -32,13 +33,13 @@ public function testConstructorInitializesComponents() public function testVerifyThrowsExceptionWithoutToken() { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(ValidationException::class); $this->sdk->verify(null); } public function testRefreshSessionThrowsExceptionWithoutToken() { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(ValidationException::class); $this->sdk->refreshSession(null); } } From 926114d770907b70dbc0b8bdfd3f291ba2b7aee6 Mon Sep 17 00:00:00 2001 From: Kevin J Gao <32936811+gaokevin1@users.noreply.github.com> Date: Thu, 2 Jan 2025 14:37:22 -0800 Subject: [PATCH 21/21] fixed linter --- README.md | 2 +- src/SDK/Cache/APCuCache.php | 2 +- src/SDK/Cache/CacheInterface.php | 2 +- src/SDK/Cache/LaravelCache.php | 23 ----------------------- src/SDK/Cache/NullCache.php | 2 +- src/SDK/Exception/ValidationException.php | 2 +- 6 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 src/SDK/Cache/LaravelCache.php diff --git a/README.md b/README.md index a9ebc22..03a24d9 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ class LaravelCache implements CacheInterface public function set(string $key, $value, int $ttl = 3600): bool { // Laravel TTL is in minutes - return Cache::put($key, $value, $ttl / 60); + return Cache::put($key, $value, max(1, ceil($ttl / 60))); } public function delete(string $key): bool diff --git a/src/SDK/Cache/APCuCache.php b/src/SDK/Cache/APCuCache.php index 17e3d1a..082ae58 100644 --- a/src/SDK/Cache/APCuCache.php +++ b/src/SDK/Cache/APCuCache.php @@ -18,4 +18,4 @@ public function delete(string $key): bool { return apcu_delete($key); } -} \ No newline at end of file +} diff --git a/src/SDK/Cache/CacheInterface.php b/src/SDK/Cache/CacheInterface.php index 6ec9d71..d7c6414 100644 --- a/src/SDK/Cache/CacheInterface.php +++ b/src/SDK/Cache/CacheInterface.php @@ -7,4 +7,4 @@ interface CacheInterface public function get(string $key); public function set(string $key, $value, int $ttl = 3600): bool; public function delete(string $key): bool; -} \ No newline at end of file +} diff --git a/src/SDK/Cache/LaravelCache.php b/src/SDK/Cache/LaravelCache.php deleted file mode 100644 index 7fa9847..0000000 --- a/src/SDK/Cache/LaravelCache.php +++ /dev/null @@ -1,23 +0,0 @@ -