Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3eb2236
chore: bump minimum PHP to 8.1 and modernize dependencies
maksimovic Mar 16, 2026
55076da
chore: drop legacy Mongo, modernize Couchbase SDK 4.x, add DynamoDB L…
maksimovic Mar 16, 2026
2ed7b46
chore: add Couchbase stub for PHPStan and phpstan.neon config
maksimovic Mar 16, 2026
3c310b1
fix: use wget for DynamoDB Local health check (image has no curl)
maksimovic Mar 16, 2026
0606175
chore: require firebase/php-jwt ^7.0, drop yoast/phpunit-polyfills, f…
maksimovic Mar 16, 2026
7275aec
chore: add composer scripts for test, coverage, and static analysis
maksimovic Mar 16, 2026
fc460b4
perf: start Cassandra and DynamoDB in background to speed up CI
maksimovic Mar 16, 2026
f213c5a
fix: use PDO for Postgres setup instead of shell commands
maksimovic Mar 16, 2026
4979b65
fix: resolve CI test deprecations and skipped storage backends
maksimovic Mar 16, 2026
6c22c2c
fix: require mongodb/mongodb ^1.20 || ^2.0 for ext-mongodb 2.x compat
maksimovic Mar 16, 2026
bc5e0ed
feat: add Couchbase Server to CI
maksimovic Mar 16, 2026
c0f201a
fix: JwtAccessTokenTest was checking wrong type (PublicKey vs PublicK…
maksimovic Mar 16, 2026
4e1b702
fix: use correct Couchbase Docker image (couchbase/server:community-7…
maksimovic Mar 16, 2026
c6c7435
revert: remove Couchbase from CI (ext-couchbase PECL build hangs)
maksimovic Mar 16, 2026
f7e36a4
fix: DynamoDB public key methods crash on null client_id
maksimovic Mar 16, 2026
97d0c06
feat: add custom Docker CI images with ext-couchbase pre-installed
maksimovic Mar 16, 2026
7f16b47
feat: add PHP 8.5 to Docker CI images, remove separate test-php85 job
maksimovic Mar 16, 2026
bafba2f
chore: remove unused intl extension from CI Docker image
maksimovic Mar 16, 2026
c09475c
perf: add GHA layer caching to Docker CI image builds
maksimovic Mar 16, 2026
571181c
debug: verify couchbase extension loads in Docker image after cleanup
maksimovic Mar 16, 2026
91805d9
ci: trigger test run with updated Docker images
maksimovic Mar 16, 2026
78de386
debug: add extension diagnostics to CI
maksimovic Mar 17, 2026
356544e
fix: use scanelf to detect and preserve runtime deps for PHP extensions
maksimovic Mar 17, 2026
91479b6
fix: couchbase 4.x classes come from composer package, not extension
maksimovic Mar 17, 2026
bf89eb4
fix: add missing unsetAccessToken to CouchbaseDB, fix test getMessage…
maksimovic Mar 17, 2026
7f92652
fix: CouchbaseDB unsetAccessToken should return false for missing tokens
maksimovic Mar 17, 2026
aa7580c
chore: move mongodb/mongodb from require-dev to suggest
maksimovic Mar 17, 2026
91747b4
ci: hardcode GHCR image namespace for cross-repo compatibility
maksimovic Mar 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
ARG PHP_VERSION=8.3
FROM php:${PHP_VERSION}-cli-alpine

RUN apk add --no-cache --virtual .build-deps \
$PHPIZE_DEPS \
cmake \
linux-headers \
openssl-dev \
zlib-dev \
libpq-dev \
&& apk add --no-cache \
git \
unzip \
libpq \
libstdc++ \
&& docker-php-ext-install -j$(nproc) \
pdo_mysql \
pdo_pgsql \
&& pecl install mongodb redis couchbase \
&& docker-php-ext-enable mongodb redis couchbase \
&& runDeps="$( \
scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \
| tr ',' '\n' | sort -u \
| awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \
)" \
&& apk add --no-cache $runDeps \
&& apk del .build-deps \
&& rm -rf /tmp/* /var/cache/apk/* \
&& php -m | grep -q couchbase \
&& php -m | grep -q mongodb

COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

WORKDIR /app
50 changes: 50 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: Build CI Images
on:
workflow_dispatch:
schedule:
# Weekly rebuild to pick up PHP patch updates
- cron: '0 6 * * 1'
push:
branches:
- main
- php85
paths:
- '.docker/Dockerfile'
- '.github/workflows/docker.yml'

env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/oauth2-php-ci

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
matrix:
php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
name: "Build PHP ${{ matrix.php }}"
steps:
- uses: actions/checkout@v5

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push
uses: docker/build-push-action@v6
with:
context: .docker
build-args: PHP_VERSION=${{ matrix.php }}
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.php }}
cache-from: type=gha,scope=php-${{ matrix.php }}
cache-to: type=gha,mode=max,scope=php-${{ matrix.php }}
90 changes: 52 additions & 38 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,68 +7,82 @@ on:
jobs:
test:
runs-on: ubuntu-latest
container:
image: ghcr.io/maksimovic/oauth2-php-ci:${{ matrix.php }}
services:
redis:
image: redis
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
mongodb:
image: mongo
ports:
- 27017:27017
myriadb:
options: --health-cmd="mongosh --eval 'db.runCommand(\"ping\").ok'" --health-interval=10s --health-timeout=5s --health-retries=5
mariadb:
image: mariadb
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3808:3808
- 3306:3306
options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=5
postgres:
image: postgres
env:
POSTGRES_DB: oauth2_server_php
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: --health-cmd="pg_isready -h localhost" --health-interval=10s --health-timeout=5s --health-retries=5
cassandra:
image: cassandra:4
options: --health-cmd="cqlsh -e 'describe cluster'" --health-interval=15s --health-timeout=10s --health-retries=10
dynamodb:
image: amazon/dynamodb-local
couchbase:
image: couchbase/server:community-7.6.2
strategy:
matrix:
php: [ 7.2, 7.3, 7.4, "8.0", 8.1, 8.2 ]
php: [ '8.1', '8.2', '8.3', '8.4', '8.5' ]
name: "PHP ${{ matrix.php }} Unit Test"
env:
MYSQL_HOST: mariadb
POSTGRES_HOST: postgres
REDIS_HOST: redis
MONGODB_HOST: mongodb
CASSANDRA_HOST: cassandra
DYNAMODB_ENDPOINT: http://dynamodb:8000
CB_CONNECTION_STRING: couchbase://couchbase
CB_USERNAME: Administrator
CB_PASSWORD: password
CB_BUCKET: oauth2test
steps:
- uses: actions/checkout@v2
- uses: codecov/codecov-action@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mongodb, mbstring, intl, redis, pdo_mysql
- uses: actions/checkout@v5
- name: Install composer dependencies
uses: nick-invision/retry@v1
with:
timeout_minutes: 10
max_attempts: 3
command: composer install
run: |
composer install --no-interaction
composer require mongodb/mongodb:'^1.20 || ^2.0' couchbase/couchbase:^4.4 --no-interaction
- name: Setup Couchbase
run: |
echo "Waiting for Couchbase..."
timeout 90 sh -c 'until wget -qO- http://couchbase:8091/ui/index.html > /dev/null 2>&1; do sleep 2; done'
echo "Couchbase responding"

wget -qO- --post-data 'clusterName=ci&services=kv&memoryQuota=256&afamily=ipv4&afamilyOnly=false&nodeEncryption=off&username=Administrator&password=password&port=SAME&sendStats=false' \
http://couchbase:8091/clusterInit
echo "Couchbase cluster initialized"

wget -qO- --post-data 'name=oauth2test&ramQuota=128&bucketType=couchbase' \
--header="Authorization: Basic $(echo -n Administrator:password | base64)" \
http://couchbase:8091/pools/default/buckets
echo "Couchbase bucket created"

timeout 30 sh -c 'until wget -qO- --header="Authorization: Basic $(echo -n Administrator:password | base64)" http://couchbase:8091/pools/default/buckets/oauth2test 2>/dev/null | grep -q "\"status\":\"healthy\""; do sleep 2; done'
echo "Couchbase ready"
- name: Run PHPUnit
run: vendor/bin/phpunit -v
run: composer test

phpstan:
name: "PHPStan"
runs-on: ubuntu-latest
container:
image: ghcr.io/maksimovic/oauth2-php-ci:8.3
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
- uses: actions/checkout@v5
- name: Install composer dependencies
uses: nick-invision/retry@v1
with:
timeout_minutes: 10
max_attempts: 3
command: composer install
run: composer install --no-interaction
- name: Run PHPStan
run: |
composer require phpstan/phpstan
vendor/bin/phpstan analyse --level=0 src/
run: composer analyze
28 changes: 17 additions & 11 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,27 @@
"psr-0": { "OAuth2": "src/" }
},
"require":{
"php":">=7.2"
"php":"^8.1"
},
"require-dev": {
"phpunit/phpunit": "^7.5||^8.0",
"aws/aws-sdk-php": "^2.8",
"firebase/php-jwt": "^6.4",
"predis/predis": "^1.1",
"thobbs/phpcassa": "dev-master",
"yoast/phpunit-polyfills": "^1.0"
"phpunit/phpunit": "^10.5",
"aws/aws-sdk-php": "^3.0",
"firebase/php-jwt": "^7.0",
"predis/predis": "^2.0",
"mroosz/php-cassandra": "^1.2",
"phpstan/phpstan": "^2.1"
},
"scripts": {
"test": "phpunit",
"test:coverage": "phpunit --coverage-text",
"analyze": "phpstan analyse --memory-limit=512M"
},
"suggest": {
"predis/predis": "Required to use Redis storage",
"thobbs/phpcassa": "Required to use Cassandra storage",
"aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage",
"firebase/php-jwt": "~v6.4 is required to use JWT features",
"mongodb/mongodb": "^1.1 is required to use MongoDB storage"
"mroosz/php-cassandra": "^1.2 is required to use Cassandra storage",
"aws/aws-sdk-php": "^3.0 is required to use DynamoDB storage",
"firebase/php-jwt": "^7.0 is required to use JWT features",
"mongodb/mongodb": "^1.20 || ^2.0 is required to use MongoDB storage",
"couchbase/couchbase": "^4.4 is required to use Couchbase storage (requires ext-couchbase)"
}
}
6 changes: 6 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
parameters:
level: 0
paths:
- src/
scanFiles:
- stubs/couchbase.stub
16 changes: 8 additions & 8 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>

<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
bootstrap="test/bootstrap.php"
cacheDirectory=".phpunit.cache"
displayDetailsOnTestsThatTriggerDeprecations="true"
displayDetailsOnTestsThatTriggerWarnings="true"
displayDetailsOnSkippedTests="true"
>
<testsuites>
<testsuite name="Oauth2 Test Suite">
<directory>./test/OAuth2/</directory>
</testsuite>
</testsuites>

<filter>
<whitelist>
<source>
<include>
<directory suffix=".php">./src/OAuth2/</directory>
</whitelist>
</filter>
</include>
</source>
</phpunit>
2 changes: 1 addition & 1 deletion src/OAuth2/Controller/AuthorizeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ public function validateAuthorizeRequest(RequestInterface $request, ResponseInte
$response_type = $request->query('response_type', $request->request('response_type'));

// for multiple-valued response types - make them alphabetical
if (false !== strpos($response_type, ' ')) {
if (false !== strpos((string) $response_type, ' ')) {
$types = explode(' ', $response_type);
sort($types);
$response_type = ltrim(implode(' ', $types));
Expand Down
10 changes: 5 additions & 5 deletions src/OAuth2/Controller/TokenController.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@ public function handleTokenRequest(RequestInterface $request, ResponseInterface
*/
public function grantAccessToken(RequestInterface $request, ResponseInterface $response)
{
if (strtolower($request->server('REQUEST_METHOD')) === 'options') {
if (strtolower((string) $request->server('REQUEST_METHOD')) === 'options') {
$response->addHttpHeaders(array('Allow' => 'POST, OPTIONS'));

return null;
}

if (strtolower($request->server('REQUEST_METHOD')) !== 'post') {
if (strtolower((string) $request->server('REQUEST_METHOD')) !== 'post') {
$response->setError(405, 'invalid_request', 'The request method must be POST when requesting an access token', '#section-3.2');
$response->addHttpHeaders(array('Allow' => 'POST, OPTIONS'));

Expand Down Expand Up @@ -263,7 +263,7 @@ public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
$identifier = $grantType->getQueryStringIdentifier();
}

$this->grantTypes[$identifier] = $grantType;
$this->grantTypes[(string) $identifier] = $grantType;
}

/**
Expand Down Expand Up @@ -293,13 +293,13 @@ public function handleRevokeRequest(RequestInterface $request, ResponseInterface
*/
public function revokeToken(RequestInterface $request, ResponseInterface $response)
{
if (strtolower($request->server('REQUEST_METHOD')) === 'options') {
if (strtolower((string) $request->server('REQUEST_METHOD')) === 'options') {
$response->addHttpHeaders(array('Allow' => 'POST, OPTIONS'));

return null;
}

if (strtolower($request->server('REQUEST_METHOD')) !== 'post') {
if (strtolower((string) $request->server('REQUEST_METHOD')) !== 'post') {
$response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2');
$response->addHttpHeaders(array('Allow' => 'POST, OPTIONS'));

Expand Down
2 changes: 1 addition & 1 deletion src/OAuth2/OpenID/GrantType/AuthorizationCode.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public function createAccessToken(AccessTokenInterface $accessToken, $client_id,
if (isset($this->authCode['id_token'])) {
// OpenID Connect requests include the refresh token only if the
// offline_access scope has been requested and granted.
$scopes = explode(' ', trim($scope));
$scopes = explode(' ', trim((string) $scope));
$includeRefreshToken = in_array('offline_access', $scopes);
}

Expand Down
4 changes: 2 additions & 2 deletions src/OAuth2/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ public function __construct($storage = null)
*/
public function checkScope($required_scope, $available_scope)
{
$required_scope = explode(' ', trim($required_scope));
$available_scope = explode(' ', trim($available_scope));
$required_scope = explode(' ', trim((string) $required_scope));
$available_scope = explode(' ', trim((string) $available_scope));

return (count(array_diff($required_scope, $available_scope)) == 0);
}
Expand Down
6 changes: 3 additions & 3 deletions src/OAuth2/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
$identifier = $grantType->getQueryStringIdentifier();
}

$this->grantTypes[$identifier] = $grantType;
$this->grantTypes[(string) $identifier] = $grantType;

// persist added grant type down to TokenController
if (!is_null($this->tokenController)) {
Expand All @@ -473,7 +473,7 @@ public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
public function addStorage($storage, $key = null)
{
// if explicitly set to a valid key, do not "magically" set below
if (isset($this->storageMap[$key])) {
if (!is_null($key) && isset($this->storageMap[$key])) {
if (!is_null($storage) && !$storage instanceof $this->storageMap[$key]) {
throw new \InvalidArgumentException(sprintf('storage of type "%s" must implement interface "%s"', $key, $this->storageMap[$key]));
}
Expand Down Expand Up @@ -516,7 +516,7 @@ public function addResponseType(ResponseTypeInterface $responseType, $key = null
{
$key = $this->normalizeResponseType($key);

if (isset($this->responseTypeMap[$key])) {
if (!is_null($key) && isset($this->responseTypeMap[$key])) {
if (!$responseType instanceof $this->responseTypeMap[$key]) {
throw new \InvalidArgumentException(sprintf('responseType of type "%s" must implement interface "%s"', $key, $this->responseTypeMap[$key]));
}
Expand Down
Loading