From 8fd2068f4e2b8b2daed122b750e8ace7cbdf7789 Mon Sep 17 00:00:00 2001 From: Joao M Date: Sun, 9 Nov 2025 20:54:49 -0300 Subject: [PATCH 1/3] Claude/update legacy config 011 c uy cef h lgxfv x zm77h5 c6 (#18) * Update legacy configuration files - Update phpunit.xml.dist to PHPUnit 10+ format with new attributes and source tag - Create psalm.xml with static analysis configuration - Update composer.json: PHP >=8.1 <8.5, byjg/jwt-wrapper ^6.0, phpunit ^10|^11, vimeo/psalm ^5.9|^6.12 - Add composer scripts for test and psalm - Update GitHub workflow to test PHP 8.1-8.4 - Update .gitignore with additional patterns * Fix compatibility with jwt-wrapper 6.0 - Update namespace imports from ByJG\Util to ByJG\JwtWrapper - Update class names: JwtKeySecret -> JwtHashHmacSecret, JwtRsaKey -> JwtOpenSSLKey - Update JwtWrapper::createJwtData() call to pass array instead of string - Make test data providers static for PHPUnit 10+ compatibility - Convert @dataProvider annotations to PHP attributes - Suppress unserialize warnings in session parser (expected behavior) * Fix psalm static analysis errors - Add #[Override] attributes to SessionHandlerInterface methods - Fix gc() return type from bool to int|false per interface - Remove redundant null coalescing for getCookiePath() calls --------- Co-authored-by: Claude --- .github/workflows/phpunit.yml | 6 ++++-- .gitignore | 3 +++ composer.json | 11 ++++++++--- phpunit.xml.dist | 27 +++++++++++++++++---------- psalm.xml | 18 ++++++++++++++++++ src/JwtSession.php | 28 +++++++++++++++++----------- src/SessionConfig.php | 12 ++++++------ tests/JwtSessionTest.php | 25 ++++++------------------- 8 files changed, 79 insertions(+), 51 deletions(-) create mode 100644 psalm.xml diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index c4473dc..81a3084 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -16,9 +16,10 @@ jobs: strategy: matrix: php-version: + - "8.4" + - "8.3" - "8.2" - "8.1" - - "8.0" steps: - uses: actions/checkout@v4 @@ -32,5 +33,6 @@ jobs: with: folder: php project: ${{ github.event.repository.name }} - secrets: inherit + secrets: + DOC_TOKEN: ${{ secrets.DOC_TOKEN }} diff --git a/.gitignore b/.gitignore index 13c8a0e..071730b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ composer.lock vendor /.phpunit.result.cache +phpunit.coverage.xml +phpunit.report.xml +*.bak diff --git a/composer.json b/composer.json index ad5b53b..6344eb8 100644 --- a/composer.json +++ b/composer.json @@ -9,11 +9,16 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": ">=8.0", - "byjg/jwt-wrapper": "4.9.*" + "php": ">=8.1 <8.5", + "byjg/jwt-wrapper": "^6.0" }, "require-dev": { - "phpunit/phpunit": "5.7.*|7.4.*|^9.6" + "phpunit/phpunit": "^10|^11", + "vimeo/psalm": "^5.9|^6.12" + }, + "scripts": { + "test": "vendor/bin/phpunit", + "psalm": "vendor/bin/psalm" }, "license": "MIT" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d5df793..0de38db 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,14 +6,21 @@ and open the template in the editor. --> - + displayDetailsOnTestsThatTriggerDeprecations="true" + displayDetailsOnTestsThatTriggerErrors="true" + displayDetailsOnTestsThatTriggerNotices="true" + displayDetailsOnTestsThatTriggerWarnings="true" + displayDetailsOnPhpunitDeprecations="true" + failOnWarning="true" + failOnNotice="true" + failOnDeprecation="true" + failOnPhpunitDeprecation="true" + stopOnFailure="false" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"> @@ -21,11 +28,11 @@ and open the template in the editor. - - + + ./src - - + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..cba57d1 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/src/JwtSession.php b/src/JwtSession.php index b417d6d..3afe776 100644 --- a/src/JwtSession.php +++ b/src/JwtSession.php @@ -2,8 +2,8 @@ namespace ByJG\Session; -use ByJG\Util\JwtWrapper; -use ByJG\Util\JwtWrapperException; +use ByJG\JwtWrapper\JwtWrapper; +use ByJG\JwtWrapper\JwtWrapperException; use Exception; use SessionHandlerInterface; @@ -64,6 +64,7 @@ protected function replaceSessionHandler(): void *

* @since 5.4.0 */ + #[\Override] public function close(): bool { return true; @@ -80,6 +81,7 @@ public function close(): bool *

* @since 5.4.0 */ + #[\Override] public function destroy(string $id): bool { if (!headers_sent()) { @@ -87,7 +89,7 @@ public function destroy(string $id): bool self::COOKIE_PREFIX . $this->sessionConfig->getSessionContext(), "", (time()-3000), - $this->sessionConfig->getCookiePath() ?? "", + $this->sessionConfig->getCookiePath(), $this->sessionConfig->getCookieDomain() ?? "", ); } @@ -99,19 +101,20 @@ public function destroy(string $id): bool * Cleanup old sessions * * @link http://php.net/manual/en/sessionhandlerinterface.gc.php + * * @param int $max_lifetime

* Sessions that have not updated for * the last maxlifetime seconds will be removed. *

- * @return int|false

- * The return value (usually TRUE on success, FALSE on failure). - * Note this value is returned internally to PHP for processing. - *

+ * + * @return int|false

The return value (usually TRUE on success, FALSE on failure). Note this value is returned internally to PHP for processing.

+ * * @since 5.4.0 */ + #[\Override] public function gc(int $max_lifetime): int|false { - return true; + return 1; } /** @@ -126,6 +129,7 @@ public function gc(int $max_lifetime): int|false *

* @since 5.4.0 */ + #[\Override] public function open(string $path, string $name): bool { return true; @@ -143,6 +147,7 @@ public function open(string $path, string $name): bool *

* @since 5.4.0 */ + #[\Override] public function read(string $id): string { try { @@ -184,13 +189,14 @@ public function read(string $id): string * @throws JwtWrapperException * @since 5.4.0 */ + #[\Override] public function write(string $id, string $data): bool { $jwt = new JwtWrapper( $this->sessionConfig->getServerName(), $this->sessionConfig->getKey() ); - $session_data = $jwt->createJwtData($data, $this->sessionConfig->getTimeoutMinutes() * 60); + $session_data = $jwt->createJwtData(['data' => $data], $this->sessionConfig->getTimeoutMinutes() * 60, 0, null); $token = $jwt->generateToken($session_data); if (!headers_sent()) { @@ -198,7 +204,7 @@ public function write(string $id, string $data): bool self::COOKIE_PREFIX . $this->sessionConfig->getSessionContext(), $token, (time()+$this->sessionConfig->getTimeoutMinutes()*60) , - $this->sessionConfig->getCookiePath() ?? "", + $this->sessionConfig->getCookiePath(), $this->sessionConfig->getCookieDomain() ?? "", false, true @@ -236,7 +242,7 @@ public function unSerializeSessionData($session_data): array $num = $pos - $offset; $varname = substr($session_data, $offset, $num); $offset += $num + 1; - $data = unserialize(substr($session_data, $offset)); + $data = @unserialize(substr($session_data, $offset), ['allowed_classes' => true]); $return_data[$varname] = $data; $offset += strlen(serialize($data)); } diff --git a/src/SessionConfig.php b/src/SessionConfig.php index cdcfdc9..54d0d13 100644 --- a/src/SessionConfig.php +++ b/src/SessionConfig.php @@ -2,9 +2,9 @@ namespace ByJG\Session; -use ByJG\Util\JwtKeyInterface; -use ByJG\Util\JwtKeySecret; -use ByJG\Util\JwtRsaKey; +use ByJG\JwtWrapper\JwtKeyInterface; +use ByJG\JwtWrapper\JwtHashHmacSecret; +use ByJG\JwtWrapper\JwtOpenSSLKey; class SessionConfig { @@ -53,13 +53,13 @@ public function withCookie($domain, $path = "/"): static public function withSecret($secret): static { - $this->jwtKey = new JwtKeySecret($secret); + $this->jwtKey = new JwtHashHmacSecret($secret); return $this; } - + public function withRsaSecret($private, $public): static { - $this->jwtKey = new JwtRsaKey($private, $public); + $this->jwtKey = new JwtOpenSSLKey($private, $public); return $this; } diff --git a/tests/JwtSessionTest.php b/tests/JwtSessionTest.php index 0cf1a09..5387a17 100644 --- a/tests/JwtSessionTest.php +++ b/tests/JwtSessionTest.php @@ -1,10 +1,11 @@ assertTrue($this->object->close()); } - public function dataProvider(): array + public static function dataProvider(): array { $obj = new stdClass(); $obj->prop1 = "value1"; @@ -119,35 +120,21 @@ public function dataProvider(): array ]; } - /** - * @dataProvider dataProvider - * @param $input - * @param $expected - */ + #[DataProvider('dataProvider')] public function testSerializeSessionData($input, $expected) { $result = $this->object->serializeSessionData($input); $this->assertEquals($expected, $result); } - /** - * @dataProvider dataProvider - * @param $expected - * @param $input - * @throws Exception - */ + #[DataProvider('dataProvider')] public function testUnserializeData($expected, $input) { $result = $this->object->unSerializeSessionData($input); $this->assertEquals($expected, $result); } - /** - * @dataProvider dataProvider - * @param $object - * @param $serialize - * @throws JwtWrapperException - */ + #[DataProvider('dataProvider')] public function testReadWrite($object, $serialize) { $this->object->write("SESSID", $serialize); From 68ee7250725449b3fbac084d6300c97a700c7ed1 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 18 Nov 2025 18:06:27 -0500 Subject: [PATCH 2/3] Update PHPUnit GitHub Actions workflow with container options and checkout version --- .github/workflows/phpunit.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 81a3084..2b4481e 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -12,7 +12,9 @@ on: jobs: Build: runs-on: 'ubuntu-latest' - container: 'byjg/php:${{ matrix.php-version }}-cli' + container: + image: 'byjg/php:${{ matrix.php-version }}-cli' + options: --user root --privileged strategy: matrix: php-version: @@ -22,7 +24,7 @@ jobs: - "8.1" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - run: composer install - run: ./vendor/bin/phpunit From a7e68370e0d801f2f36b72e8a24587d4aa98b765 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 22 Nov 2025 01:43:17 -0500 Subject: [PATCH 3/3] Update GitHub Actions, dependencies, and improve JWT session handling - Added Psalm static analysis to GitHub Actions workflow. - Updated PHP version matrix in workflows to include PHP 8.5 and removed older versions. - Enhanced session key validation in `JwtSession` methods. - Adjusted composer constraints for PHP and dependencies. - Updated `README.md` with revised description and sponsor badge. - Improved error handling in `JwtSession` implementation. - Bumped Psalm error level to 3 in configuration for stricter checks. --- .github/workflows/phpunit.yml | 35 ++++++++++++++++++++++++++++++++--- README.md | 14 +++++++------- composer.json | 10 +++++----- psalm.xml | 2 +- src/JwtSession.php | 17 ++++++++++++++--- tests/JwtSessionTest.php | 2 +- 6 files changed, 60 insertions(+), 20 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 2b4481e..fc5f29a 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -18,15 +18,44 @@ jobs: strategy: matrix: php-version: + - "8.5" - "8.4" - "8.3" - - "8.2" - - "8.1" steps: - uses: actions/checkout@v5 - run: composer install - - run: ./vendor/bin/phpunit + - run: composer test + + Psalm: + name: Psalm Static Analyzer + runs-on: ubuntu-latest + permissions: + # for github/codeql-action/upload-sarif to upload SARIF results + security-events: write + container: + image: byjg/php:8.4-cli + options: --user root --privileged + + steps: + - name: Git checkout + uses: actions/checkout@v4 + + - name: Composer + run: composer install + + - name: Psalm + # Note: Ignoring error code 2, which just signals that some + # flaws were found, not that Psalm itself failed to run. + run: ./vendor/bin/psalm + --show-info=true + --report=psalm-results.sarif || [ $? = 2 ] + + - name: Upload Analysis results to GitHub + uses: github/codeql-action/upload-sarif@v4 + if: github.ref == 'refs/heads/master' + with: + sarif_file: psalm-results.sarif Documentation: if: github.ref == 'refs/heads/master' diff --git a/README.md b/README.md index c92de21..421c5cd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# JwtSession +# JWT Session Handler -[![Build Status](https://github.com/byjg/jwt-session/actions/workflows/phpunit.yml/badge.svg?branch=master)](https://github.com/byjg/jwt-session/actions/workflows/phpunit.yml) -[![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) -[![GitHub source](https://img.shields.io/badge/Github-source-informational?logo=github)](https://github.com/byjg/jwt-session/) -[![GitHub license](https://img.shields.io/github/license/byjg/jwt-session.svg)](https://opensource.byjg.com/opensource/licensing.html) +[![Sponsor](https://img.shields.io/badge/Sponsor-%23ea4aaa?logo=githubsponsors&logoColor=white&labelColor=0d1117)](https://github.com/sponsors/byjg) +[![Build Status](https://github.com/byjg/jwt-session/actions/workflows/phpunit.yml/badge.svg?branch=master)](https://github.com/byjg/jwt-session/actions/workflows/phpunit.yml) +[![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) +[![GitHub source](https://img.shields.io/badge/Github-source-informational?logo=github)](https://github.com/byjg/jwt-session/) +[![GitHub license](https://img.shields.io/github/license/byjg/jwt-session.svg)](https://opensource.byjg.com/opensource/licensing.html) [![GitHub release](https://img.shields.io/github/release/byjg/jwt-session.svg)](https://github.com/byjg/jwt-session/releases/) -JwtSession is a PHP session replacement. Instead of use FileSystem, just use JWT TOKEN. -The implementation following the SessionHandlerInterface. +A PHP session replacement that stores session data in JWT tokens instead of the filesystem. This implementation follows the SessionHandlerInterface standard, enabling stateless sessions without the need for dedicated session servers like Redis or Memcached. Perfect for distributed applications and microservices architectures. # How to use: diff --git a/composer.json b/composer.json index 6344eb8..ac59705 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "byjg/jwt-session", - "description": "JwtSession is a PHP session replacement. Instead of use FileSystem, just use JWT TOKEN. The implementation following the SessionHandlerInterface.", + "description": "A PHP session replacement that stores session data in JWT tokens instead of the filesystem. This implementation follows the SessionHandlerInterface standard, enabling stateless sessions without the need for dedicated session servers like Redis or Memcached. Perfect for distributed applications and microservices architectures.", "autoload": { "psr-4": { "ByJG\\Session\\": "src/" @@ -9,16 +9,16 @@ "minimum-stability": "dev", "prefer-stable": true, "require": { - "php": ">=8.1 <8.5", + "php": ">=8.3 <8.6", "byjg/jwt-wrapper": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^10|^11", - "vimeo/psalm": "^5.9|^6.12" + "phpunit/phpunit": "^10.5|^11.5", + "vimeo/psalm": "^5.9|^6.13" }, "scripts": { "test": "vendor/bin/phpunit", - "psalm": "vendor/bin/psalm" + "psalm": "vendor/bin/psalm --threads=1" }, "license": "MIT" } diff --git a/psalm.xml b/psalm.xml index cba57d1..241b742 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,6 +1,6 @@ sessionConfig->getSessionContext()])) { + $key = $this->sessionConfig->getKey(); + if ($key === null) { + return ''; + } $jwt = new JwtWrapper( $this->sessionConfig->getServerName(), - $this->sessionConfig->getKey() + $key ); $data = $jwt->extractData($_COOKIE[self::COOKIE_PREFIX . $this->sessionConfig->getSessionContext()]); @@ -192,9 +196,13 @@ public function read(string $id): string #[\Override] public function write(string $id, string $data): bool { + $key = $this->sessionConfig->getKey(); + if ($key === null) { + return false; + } $jwt = new JwtWrapper( $this->sessionConfig->getServerName(), - $this->sessionConfig->getKey() + $key ); $session_data = $jwt->createJwtData(['data' => $data], $this->sessionConfig->getTimeoutMinutes() * 60, 0, null); $token = $jwt->generateToken($session_data); @@ -239,6 +247,9 @@ public function unSerializeSessionData($session_data): array while ($offset < strlen($session_data)) { if (!str_contains(substr($session_data, $offset), "|")) throw new JwtSessionException("invalid data, remaining: " . substr($session_data, $offset)); $pos = strpos($session_data, "|", $offset); + if ($pos === false) { + throw new JwtSessionException("invalid data, pipe not found"); + } $num = $pos - $offset; $varname = substr($session_data, $offset, $num); $offset += $num + 1; diff --git a/tests/JwtSessionTest.php b/tests/JwtSessionTest.php index 5387a17..47fbb82 100644 --- a/tests/JwtSessionTest.php +++ b/tests/JwtSessionTest.php @@ -22,7 +22,7 @@ class JwtSessionTest extends TestCase */ protected SessionConfig $sessionConfig; - const SESSION_ID = "sessionid"; + const string SESSION_ID = "sessionid"; /** * @throws JwtSessionException