diff --git a/.docker/Dockerfile b/.docker/Dockerfile new file mode 100644 index 000000000..4c8a13028 --- /dev/null +++ b/.docker/Dockerfile @@ -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 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..c3c1202c8 --- /dev/null +++ b/.github/workflows/docker.yml @@ -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 }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe13e48b8..a7bfadddd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 diff --git a/composer.json b/composer.json index 50a950774..66cc9cbd2 100644 --- a/composer.json +++ b/composer.json @@ -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)" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 000000000..d0a164b61 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 0 + paths: + - src/ + scanFiles: + - stubs/couchbase.stub diff --git a/phpunit.xml b/phpunit.xml index 6e663e39d..66b040c02 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,14 +1,14 @@ @@ -16,9 +16,9 @@ - - + + ./src/OAuth2/ - - + + diff --git a/src/OAuth2/Controller/AuthorizeController.php b/src/OAuth2/Controller/AuthorizeController.php index 181f884a6..93e5b782f 100644 --- a/src/OAuth2/Controller/AuthorizeController.php +++ b/src/OAuth2/Controller/AuthorizeController.php @@ -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)); diff --git a/src/OAuth2/Controller/TokenController.php b/src/OAuth2/Controller/TokenController.php index 1e21b5f67..01c8798ba 100644 --- a/src/OAuth2/Controller/TokenController.php +++ b/src/OAuth2/Controller/TokenController.php @@ -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')); @@ -263,7 +263,7 @@ public function addGrantType(GrantTypeInterface $grantType, $identifier = null) $identifier = $grantType->getQueryStringIdentifier(); } - $this->grantTypes[$identifier] = $grantType; + $this->grantTypes[(string) $identifier] = $grantType; } /** @@ -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')); diff --git a/src/OAuth2/OpenID/GrantType/AuthorizationCode.php b/src/OAuth2/OpenID/GrantType/AuthorizationCode.php index ee113a0e5..01e14540b 100644 --- a/src/OAuth2/OpenID/GrantType/AuthorizationCode.php +++ b/src/OAuth2/OpenID/GrantType/AuthorizationCode.php @@ -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); } diff --git a/src/OAuth2/Scope.php b/src/OAuth2/Scope.php index 3ba6e5328..b38b757a9 100644 --- a/src/OAuth2/Scope.php +++ b/src/OAuth2/Scope.php @@ -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); } diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index e5716358d..6b4881ab2 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -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)) { @@ -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])); } @@ -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])); } diff --git a/src/OAuth2/Storage/Cassandra.php b/src/OAuth2/Storage/Cassandra.php index 3a138bb52..fa4b136ab 100644 --- a/src/OAuth2/Storage/Cassandra.php +++ b/src/OAuth2/Storage/Cassandra.php @@ -2,9 +2,8 @@ namespace OAuth2\Storage; -use phpcassa\ColumnFamily; -use phpcassa\ColumnSlice; -use phpcassa\Connection\ConnectionPool; +use Cassandra\Connection; +use Cassandra\Connection\StreamNodeConfig; use OAuth2\OpenID\Storage\UserClaimsInterface; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; use InvalidArgumentException; @@ -12,14 +11,17 @@ /** * Cassandra storage for all storage types * - * To use, install "thobbs/phpcassa" via composer: + * To use, install "mroosz/php-cassandra" via composer: * - * composer require thobbs/phpcassa:dev-master + * composer require mroosz/php-cassandra * * * Once this is done, instantiate the connection: * - * $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_server', array('127.0.0.1:9160')); + * $cassandra = new \Cassandra\Connection([ + * new \Cassandra\Connection\StreamNodeConfig(host: '127.0.0.1', port: 9042), + * ], keyspace: 'oauth2'); + * $cassandra->connect(); * * * Then, register the storage client: @@ -27,8 +29,6 @@ * $storage = new OAuth2\Storage\Cassandra($cassandra); * $storage->setClientDetails($client_id, $client_secret, $redirect_uri); * - * - * @see test/lib/OAuth2/Storage/Bootstrap::getCassandraStorage */ class Cassandra implements AuthorizationCodeInterface, AccessTokenInterface, @@ -41,48 +41,50 @@ class Cassandra implements AuthorizationCodeInterface, UserClaimsInterface, OpenIDAuthorizationCodeInterface { - private $cache; - /** - * @var ConnectionPool - */ - protected $cassandra; + protected Connection $connection; - /** - * @var array - */ - protected $config; + protected array $config; /** - * Cassandra Storage! uses phpCassa - * - * @param ConnectionPool|array $connection - * @param array $config + * @param Connection|array $connection A Connection instance or a configuration array + * @param array $config * * @throws InvalidArgumentException */ - public function __construct($connection = array(), array $config = array()) + public function __construct($connection, array $config = array()) { - if ($connection instanceof ConnectionPool) { - $this->cassandra = $connection; - } else { - if (!is_array($connection)) { - throw new InvalidArgumentException('First argument to OAuth2\Storage\Cassandra must be an instance of phpcassa\Connection\ConnectionPool or a configuration array'); - } + if ($connection instanceof Connection) { + $this->connection = $connection; + } elseif (is_array($connection)) { $connection = array_merge(array( + 'host' => '127.0.0.1', + 'port' => 9042, 'keyspace' => 'oauth2', - 'servers' => null, + 'username' => null, + 'password' => null, ), $connection); - $this->cassandra = new ConnectionPool($connection['keyspace'], $connection['servers']); + $nodeConfig = new StreamNodeConfig( + host: $connection['host'], + port: $connection['port'], + username: $connection['username'], + password: $connection['password'], + ); + + $this->connection = new Connection([$nodeConfig], keyspace: $connection['keyspace']); + $this->connection->connect(); + } else { + throw new InvalidArgumentException( + 'First argument to OAuth2\Storage\Cassandra must be an instance of Cassandra\Connection or a configuration array' + ); } $this->config = array_merge(array( - // cassandra config - 'column_family' => 'auth', + 'table' => 'oauth_data', - // key names + // key prefixes 'client_key' => 'oauth_clients:', 'access_token_key' => 'oauth_access_tokens:', 'refresh_token_key' => 'oauth_refresh_tokens:', @@ -90,107 +92,98 @@ public function __construct($connection = array(), array $config = array()) 'user_key' => 'oauth_users:', 'jwt_key' => 'oauth_jwt:', 'scope_key' => 'oauth_scopes:', - 'public_key_key' => 'oauth_public_keys:', + 'public_key_key' => 'oauth_public_keys:', ), $config); } - /** - * @param $key - * @return bool|mixed - */ protected function getValue($key) { if (isset($this->cache[$key])) { return $this->cache[$key]; } - $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); try { - $value = $cf->get($key, new ColumnSlice("", "")); - $value = array_shift($value); - } catch (\cassandra\NotFoundException $e) { + $result = $this->connection->query( + sprintf('SELECT value FROM %s WHERE key = ?', $this->config['table']), + [$key] + )->asRowsResult(); + + foreach ($result as $row) { + return json_decode($row['value'], true); + } + } catch (\Exception $e) { return false; } - return json_decode($value, true); + return false; } - /** - * @param $key - * @param $value - * @param int $expire - * @return bool - */ protected function setValue($key, $value, $expire = 0) { $this->cache[$key] = $value; - $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); - $str = json_encode($value); - if ($expire > 0) { - try { + + try { + if ($expire > 0) { $seconds = $expire - time(); - // __data key set as C* requires a field, note: max TTL can only be 630720000 seconds - $cf->insert($key, array('__data' => $str), null, $seconds); - } catch (\Exception $e) { - return false; - } - } else { - try { - // __data key set as C* requires a field - $cf->insert($key, array('__data' => $str)); - } catch (\Exception $e) { - return false; + if ($seconds < 1) { + $seconds = 1; + } + $this->connection->query( + sprintf('INSERT INTO %s (key, value) VALUES (?, ?) USING TTL %d', $this->config['table'], $seconds), + [$key, $str] + ); + } else { + $this->connection->query( + sprintf('INSERT INTO %s (key, value) VALUES (?, ?)', $this->config['table']), + [$key, $str] + ); } + } catch (\Exception $e) { + return false; } return true; } - /** - * @param $key - * @return bool - */ protected function expireValue($key) { unset($this->cache[$key]); - $cf = new ColumnFamily($this->cassandra, $this->config['column_family']); + try { + // check if the key exists before deleting + $result = $this->connection->query( + sprintf('SELECT key FROM %s WHERE key = ?', $this->config['table']), + [$key] + )->asRowsResult(); + + $found = false; + foreach ($result as $row) { + $found = true; + break; + } - if ($cf->get_count($key) > 0) { - try { - // __data key set as C* requires a field - $cf->remove($key, array('__data')); - } catch (\Exception $e) { + if (!$found) { return false; } - return true; + $this->connection->query( + sprintf('DELETE FROM %s WHERE key = ?', $this->config['table']), + [$key] + ); + } catch (\Exception $e) { + return false; } - return false; + return true; } - /** - * @param string $code - * @return bool|mixed - */ public function getAuthorizationCode($code) { return $this->getValue($this->config['code_key'] . $code); } - /** - * @param string $authorization_code - * @param mixed $client_id - * @param mixed $user_id - * @param string $redirect_uri - * @param int $expires - * @param string $scope - * @param string $id_token - * @return bool - */ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { return $this->setValue( @@ -200,10 +193,6 @@ public function setAuthorizationCode($authorization_code, $client_id, $user_id, ); } - /** - * @param string $code - * @return bool - */ public function expireAuthorizationCode($code) { $key = $this->config['code_key'] . $code; @@ -212,11 +201,6 @@ public function expireAuthorizationCode($code) return $this->expireValue($key); } - /** - * @param string $username - * @param string $password - * @return bool - */ public function checkUserCredentials($username, $password) { if ($user = $this->getUser($username)) { @@ -226,56 +210,32 @@ public function checkUserCredentials($username, $password) return false; } - /** - * plaintext passwords are bad! Override this for your application - * - * @param array $user - * @param string $password - * @return bool - */ protected function checkPassword($user, $password) { return $user['password'] == $this->hashPassword($password); } - // use a secure hashing algorithm when storing passwords. Override this for your application protected function hashPassword($password) { return sha1($password); } - /** - * @param string $username - * @return array|bool|false - */ public function getUserDetails($username) { return $this->getUser($username); } - /** - * @param string $username - * @return array|bool - */ public function getUser($username) { if (!$userInfo = $this->getValue($this->config['user_key'] . $username)) { return false; } - // the default behavior is to use "username" as the user_id return array_merge(array( 'user_id' => $username, ), $userInfo); } - /** - * @param string $username - * @param string $password - * @param string $first_name - * @param string $last_name - * @return bool - */ public function setUser($username, $password, $first_name = null, $last_name = null) { $password = $this->hashPassword($password); @@ -286,11 +246,6 @@ public function setUser($username, $password, $first_name = null, $last_name = n ); } - /** - * @param mixed $client_id - * @param string $client_secret - * @return bool - */ public function checkClientCredentials($client_id, $client_secret = null) { if (!$client = $this->getClientDetails($client_id)) { @@ -301,10 +256,6 @@ public function checkClientCredentials($client_id, $client_secret = null) && $client['client_secret'] == $client_secret; } - /** - * @param $client_id - * @return bool - */ public function isPublicClient($client_id) { if (!$client = $this->getClientDetails($client_id)) { @@ -314,24 +265,11 @@ public function isPublicClient($client_id) return empty($client['client_secret']); } - /** - * @param $client_id - * @return array|bool|mixed - */ public function getClientDetails($client_id) { return $this->getValue($this->config['client_key'] . $client_id); } - /** - * @param $client_id - * @param null $client_secret - * @param null $redirect_uri - * @param null $grant_types - * @param null $scope - * @param null $user_id - * @return bool - */ public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { return $this->setValue( @@ -340,11 +278,6 @@ public function setClientDetails($client_id, $client_secret = null, $redirect_ur ); } - /** - * @param $client_id - * @param $grant_type - * @return bool - */ public function checkRestrictedGrantType($client_id, $grant_type) { $details = $this->getClientDetails($client_id); @@ -354,27 +287,14 @@ public function checkRestrictedGrantType($client_id, $grant_type) return in_array($grant_type, (array) $grant_types); } - // if grant_types are not defined, then none are restricted return true; } - /** - * @param $refresh_token - * @return bool|mixed - */ public function getRefreshToken($refresh_token) { return $this->getValue($this->config['refresh_token_key'] . $refresh_token); } - /** - * @param $refresh_token - * @param $client_id - * @param $user_id - * @param $expires - * @param null $scope - * @return bool - */ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { return $this->setValue( @@ -384,85 +304,50 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, ); } - /** - * @param $refresh_token - * @return bool - */ public function unsetRefreshToken($refresh_token) { return $this->expireValue($this->config['refresh_token_key'] . $refresh_token); } - /** - * @param string $access_token - * @return array|bool|mixed|null - */ public function getAccessToken($access_token) { - return $this->getValue($this->config['access_token_key'].$access_token); + return $this->getValue($this->config['access_token_key'] . $access_token); } - /** - * @param string $access_token - * @param mixed $client_id - * @param mixed $user_id - * @param int $expires - * @param null $scope - * @return bool - */ public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) { return $this->setValue( - $this->config['access_token_key'].$access_token, + $this->config['access_token_key'] . $access_token, compact('access_token', 'client_id', 'user_id', 'expires', 'scope'), $expires ); } - /** - * @param $access_token - * @return bool - */ public function unsetAccessToken($access_token) { return $this->expireValue($this->config['access_token_key'] . $access_token); } - /** - * @param $scope - * @return bool - */ public function scopeExists($scope) { $scope = explode(' ', $scope); - $result = $this->getValue($this->config['scope_key'].'supported:global'); + $result = $this->getValue($this->config['scope_key'] . 'supported:global'); $supportedScope = explode(' ', (string) $result); return (count(array_diff($scope, $supportedScope)) == 0); } - /** - * @param null $client_id - * @return bool|mixed - */ public function getDefaultScope($client_id = null) { - if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'].'default:'.$client_id)) { - $result = $this->getValue($this->config['scope_key'].'default:global'); + if (is_null($client_id) || !$result = $this->getValue($this->config['scope_key'] . 'default:' . $client_id)) { + $result = $this->getValue($this->config['scope_key'] . 'default:global'); } return $result; } - /** - * @param $scope - * @param null $client_id - * @param string $type - * @return bool - * @throws \InvalidArgumentException - */ public function setScope($scope, $client_id = null, $type = 'supported') { if (!in_array($type, array('default', 'supported'))) { @@ -470,50 +355,35 @@ public function setScope($scope, $client_id = null, $type = 'supported') } if (is_null($client_id)) { - $key = $this->config['scope_key'].$type.':global'; + $key = $this->config['scope_key'] . $type . ':global'; } else { - $key = $this->config['scope_key'].$type.':'.$client_id; + $key = $this->config['scope_key'] . $type . ':' . $client_id; } return $this->setValue($key, $scope); } - /** - * @param $client_id - * @param $subject - * @return bool|null - */ public function getClientKey($client_id, $subject) { if (!$jwt = $this->getValue($this->config['jwt_key'] . $client_id)) { return false; } - if (isset($jwt['subject']) && $jwt['subject'] == $subject ) { + if (isset($jwt['subject']) && $jwt['subject'] == $subject) { return $jwt['key']; } return null; } - /** - * @param $client_id - * @param $key - * @param null $subject - * @return bool - */ public function setClientKey($client_id, $key, $subject = null) { return $this->setValue($this->config['jwt_key'] . $client_id, array( 'key' => $key, - 'subject' => $subject + 'subject' => $subject, )); } - /** - * @param $client_id - * @return bool|null - */ public function getClientScope($client_id) { if (!$clientDetails = $this->getClientDetails($client_id)) { @@ -527,38 +397,16 @@ public function getClientScope($client_id) return null; } - /** - * @param $client_id - * @param $subject - * @param $audience - * @param $expiration - * @param $jti - * @throws \Exception - */ public function getJti($client_id, $subject, $audience, $expiration, $jti) { - //TODO: Needs cassandra implementation. throw new \Exception('getJti() for the Cassandra driver is currently unimplemented.'); } - /** - * @param $client_id - * @param $subject - * @param $audience - * @param $expiration - * @param $jti - * @throws \Exception - */ public function setJti($client_id, $subject, $audience, $expiration, $jti) { - //TODO: Needs cassandra implementation. throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.'); } - /** - * @param string $client_id - * @return mixed - */ public function getPublicKey($client_id = '') { $public_key = $this->getValue($this->config['public_key_key'] . $client_id); @@ -571,10 +419,6 @@ public function getPublicKey($client_id = '') } } - /** - * @param string $client_id - * @return mixed - */ public function getPrivateKey($client_id = '') { $public_key = $this->getValue($this->config['public_key_key'] . $client_id); @@ -587,10 +431,6 @@ public function getPrivateKey($client_id = '') } } - /** - * @param null $client_id - * @return mixed|string - */ public function getEncryptionAlgorithm($client_id = null) { $public_key = $this->getValue($this->config['public_key_key'] . $client_id); @@ -605,11 +445,6 @@ public function getEncryptionAlgorithm($client_id = null) return 'RS256'; } - /** - * @param mixed $user_id - * @param string $claims - * @return array|bool - */ public function getUserClaims($user_id, $claims) { $userDetails = $this->getUserDetails($user_id); @@ -620,12 +455,10 @@ public function getUserClaims($user_id, $claims) $claims = explode(' ', trim($claims)); $userClaims = array(); - // for each requested claim, if the user has the claim, set it in the response $validClaims = explode(' ', self::VALID_CLAIMS); foreach ($validClaims as $validClaim) { if (in_array($validClaim, $claims)) { if ($validClaim == 'address') { - // address is an object with subfields $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); } else { $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); @@ -636,11 +469,6 @@ public function getUserClaims($user_id, $claims) return $userClaims; } - /** - * @param $claim - * @param $userDetails - * @return array - */ protected function getUserClaim($claim, $userDetails) { $userClaims = array(); @@ -649,7 +477,7 @@ protected function getUserClaim($claim, $userDetails) foreach ($claimValues as $value) { if ($value == 'email_verified') { - $userClaims[$value] = $userDetails[$value]=='true' ? true : false; + $userClaims[$value] = $userDetails[$value] == 'true' ? true : false; } else { $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; } @@ -657,4 +485,4 @@ protected function getUserClaim($claim, $userDetails) return $userClaims; } -} \ No newline at end of file +} diff --git a/src/OAuth2/Storage/CouchbaseDB.php b/src/OAuth2/Storage/CouchbaseDB.php index 31b0cd301..c59a104ac 100755 --- a/src/OAuth2/Storage/CouchbaseDB.php +++ b/src/OAuth2/Storage/CouchbaseDB.php @@ -2,12 +2,17 @@ namespace OAuth2\Storage; -use Couchbase; +use Couchbase\Cluster; +use Couchbase\ClusterOptions; +use Couchbase\Collection; +use Couchbase\Exception\DocumentNotFoundException; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; /** * Simple Couchbase storage for all storage types * + * Uses Couchbase SDK 4.x (ext-couchbase ^4.0). + * * This class should be extended or overridden as required * * NOTE: Passwords are stored in plaintext, which is never @@ -23,59 +28,80 @@ class CouchbaseDB implements AuthorizationCodeInterface, JwtBearerInterface, OpenIDAuthorizationCodeInterface { - protected $db; - protected $config; - - public function __construct($connection, $config = array()) + protected Collection $collection; + protected array $config; + + /** + * @param Collection|array $connection A Couchbase\Collection instance or a config array + * with keys: connection_string, username, password, bucket, scope (optional), collection (optional) + * @param array $config Table name overrides + */ + public function __construct($connection, array $config = []) { - if (!class_exists(Couchbase::class)) { - throw new \RuntimeException('Missing Couchbase'); - } - - if ($connection instanceof Couchbase) { - $this->db = $connection; + if ($connection instanceof Collection) { + $this->collection = $connection; } else { - if (!is_array($connection) || !is_array($connection['servers'])) { - throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array'); + if (!is_array($connection)) { + throw new \InvalidArgumentException( + 'First argument to OAuth2\Storage\CouchbaseDB must be a Couchbase\Collection or a configuration array' + ); } - $this->db = new Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false); + $options = new ClusterOptions(); + $options->credentials($connection['username'] ?? '', $connection['password'] ?? ''); + $cluster = new Cluster($connection['connection_string'], $options); + $bucket = $cluster->bucket($connection['bucket']); + $scope = $bucket->scope($connection['scope'] ?? '_default'); + $this->collection = $scope->collection($connection['collection'] ?? '_default'); } - $this->config = array_merge(array( + $this->config = array_merge([ 'client_table' => 'oauth_clients', 'access_token_table' => 'oauth_access_tokens', 'refresh_token_table' => 'oauth_refresh_tokens', 'code_table' => 'oauth_authorization_codes', 'user_table' => 'oauth_users', 'jwt_table' => 'oauth_jwt', - ), $config); + ], $config); } - // Helper function to access couchbase item by type: - protected function getObjectByType($name,$id) + /** + * Build a document key from a table name config key and an id. + */ + protected function buildKey(string $name, string $id): string { - return json_decode($this->db->get($this->config[$name].'-'.$id),true); + return $this->config[$name] . '-' . $id; } - // Helper function to set couchbase item by type: - protected function setObjectByType($name,$id,$array) + protected function getObjectByType(string $name, string $id): ?array { - $array['type'] = $name; + try { + $result = $this->collection->get($this->buildKey($name, $id)); + return $result->content(); + } catch (DocumentNotFoundException) { + return null; + } + } - return $this->db->set($this->config[$name].'-'.$id,json_encode($array)); + protected function setObjectByType(string $name, string $id, array $data): void + { + $data['type'] = $name; + $this->collection->upsert($this->buildKey($name, $id), $data); } - // Helper function to delete couchbase item by type, wait for persist to at least 1 node - protected function deleteObjectByType($name,$id) + protected function deleteObjectByType(string $name, string $id): void { - $this->db->delete($this->config[$name].'-'.$id,"",1); + try { + $this->collection->remove($this->buildKey($name, $id)); + } catch (DocumentNotFoundException) { + // already gone + } } /* ClientCredentialsInterface */ public function checkClientCredentials($client_id, $client_secret = null) { - if ($result = $this->getObjectByType('client_table',$client_id)) { + if ($result = $this->getObjectByType('client_table', $client_id)) { return $result['client_secret'] == $client_secret; } @@ -84,7 +110,7 @@ public function checkClientCredentials($client_id, $client_secret = null) public function isPublicClient($client_id) { - if (!$result = $this->getObjectByType('client_table',$client_id)) { + if (!$result = $this->getObjectByType('client_table', $client_id)) { return false; } @@ -94,33 +120,21 @@ public function isPublicClient($client_id) /* ClientInterface */ public function getClientDetails($client_id) { - $result = $this->getObjectByType('client_table',$client_id); + $result = $this->getObjectByType('client_table', $client_id); return is_null($result) ? false : $result; } public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { - if ($this->getClientDetails($client_id)) { - - $this->setObjectByType('client_table',$client_id, array( - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'redirect_uri' => $redirect_uri, - 'grant_types' => $grant_types, - 'scope' => $scope, - 'user_id' => $user_id, - )); - } else { - $this->setObjectByType('client_table',$client_id, array( - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'redirect_uri' => $redirect_uri, - 'grant_types' => $grant_types, - 'scope' => $scope, - 'user_id' => $user_id, - )); - } + $this->setObjectByType('client_table', $client_id, [ + 'client_id' => $client_id, + 'client_secret' => $client_secret, + 'redirect_uri' => $redirect_uri, + 'grant_types' => $grant_types, + 'scope' => $scope, + 'user_id' => $user_id, + ]); return true; } @@ -141,78 +155,63 @@ public function checkRestrictedGrantType($client_id, $grant_type) /* AccessTokenInterface */ public function getAccessToken($access_token) { - $token = $this->getObjectByType('access_token_table',$access_token); + $token = $this->getObjectByType('access_token_table', $access_token); return is_null($token) ? false : $token; } public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) { - // if it exists, update it. - if ($this->getAccessToken($access_token)) { - $this->setObjectByType('access_token_table',$access_token, array( - 'access_token' => $access_token, - 'client_id' => $client_id, - 'expires' => $expires, - 'user_id' => $user_id, - 'scope' => $scope - )); - } else { - $this->setObjectByType('access_token_table',$access_token, array( - 'access_token' => $access_token, - 'client_id' => $client_id, - 'expires' => $expires, - 'user_id' => $user_id, - 'scope' => $scope - )); - } + $this->setObjectByType('access_token_table', $access_token, [ + 'access_token' => $access_token, + 'client_id' => $client_id, + 'expires' => $expires, + 'user_id' => $user_id, + 'scope' => $scope, + ]); return true; } + public function unsetAccessToken($access_token) + { + try { + $this->collection->remove($this->buildKey('access_token_table', $access_token)); + + return true; + } catch (DocumentNotFoundException) { + return false; + } + } + /* AuthorizationCodeInterface */ public function getAuthorizationCode($code) { - $code = $this->getObjectByType('code_table',$code); + $code = $this->getObjectByType('code_table', $code); return is_null($code) ? false : $code; } public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) { - // if it exists, update it. - if ($this->getAuthorizationCode($code)) { - $this->setObjectByType('code_table',$code, array( - 'authorization_code' => $code, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'redirect_uri' => $redirect_uri, - 'expires' => $expires, - 'scope' => $scope, - 'id_token' => $id_token, - 'code_challenge' => $code_challenge, - 'code_challenge_method' => $code_challenge_method, - )); - } else { - $this->setObjectByType('code_table',$code,array( - 'authorization_code' => $code, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'redirect_uri' => $redirect_uri, - 'expires' => $expires, - 'scope' => $scope, - 'id_token' => $id_token, - 'code_challenge' => $code_challenge, - 'code_challenge_method' => $code_challenge_method, - )); - } + $this->setObjectByType('code_table', $code, [ + 'authorization_code' => $code, + 'client_id' => $client_id, + 'user_id' => $user_id, + 'redirect_uri' => $redirect_uri, + 'expires' => $expires, + 'scope' => $scope, + 'id_token' => $id_token, + 'code_challenge' => $code_challenge, + 'code_challenge_method' => $code_challenge_method, + ]); return true; } public function expireAuthorizationCode($code) { - $this->deleteObjectByType('code_table',$code); + $this->deleteObjectByType('code_table', $code); return true; } @@ -239,27 +238,27 @@ public function getUserDetails($username) /* RefreshTokenInterface */ public function getRefreshToken($refresh_token) { - $token = $this->getObjectByType('refresh_token_table',$refresh_token); + $token = $this->getObjectByType('refresh_token_table', $refresh_token); return is_null($token) ? false : $token; } public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { - $this->setObjectByType('refresh_token_table',$refresh_token, array( + $this->setObjectByType('refresh_token_table', $refresh_token, [ 'refresh_token' => $refresh_token, 'client_id' => $client_id, 'user_id' => $user_id, 'expires' => $expires, - 'scope' => $scope - )); + 'scope' => $scope, + ]); return true; } public function unsetRefreshToken($refresh_token) { - $this->deleteObjectByType('refresh_token_table',$refresh_token); + $this->deleteObjectByType('refresh_token_table', $refresh_token); return true; } @@ -272,37 +271,26 @@ protected function checkPassword($user, $password) public function getUser($username) { - $result = $this->getObjectByType('user_table',$username); + $result = $this->getObjectByType('user_table', $username); return is_null($result) ? false : $result; } public function setUser($username, $password, $firstName = null, $lastName = null) { - if ($this->getUser($username)) { - $this->setObjectByType('user_table',$username, array( - 'username' => $username, - 'password' => $password, - 'first_name' => $firstName, - 'last_name' => $lastName - )); - - } else { - $this->setObjectByType('user_table',$username, array( - 'username' => $username, - 'password' => $password, - 'first_name' => $firstName, - 'last_name' => $lastName - )); - - } + $this->setObjectByType('user_table', $username, [ + 'username' => $username, + 'password' => $password, + 'first_name' => $firstName, + 'last_name' => $lastName, + ]); return true; } public function getClientKey($client_id, $subject) { - if (!$jwt = $this->getObjectByType('jwt_table',$client_id)) { + if (!$jwt = $this->getObjectByType('jwt_table', $client_id)) { return false; } diff --git a/src/OAuth2/Storage/DynamoDB.php b/src/OAuth2/Storage/DynamoDB.php index 713189d23..eddbf7d72 100644 --- a/src/OAuth2/Storage/DynamoDB.php +++ b/src/OAuth2/Storage/DynamoDB.php @@ -3,6 +3,7 @@ namespace OAuth2\Storage; use Aws\DynamoDb\DynamoDbClient; +use Aws\DynamoDb\Marshaler; use OAuth2\OpenID\Storage\UserClaimsInterface; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; @@ -11,7 +12,7 @@ * * To use, install "aws/aws-sdk-php" via composer * - * composer require aws/aws-sdk-php:dev-master + * composer require aws/aws-sdk-php:^3.0 * * * Once this is done, instantiate the DynamoDB client @@ -45,25 +46,31 @@ class DynamoDB implements { protected $client; protected $config; + protected $marshaler; public function __construct($connection, $config = array()) { if (!($connection instanceof DynamoDbClient)) { if (!is_array($connection)) { - throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance of DynamoDbClient or a configuration array containing key, secret, region'); } if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) { - throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); + throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance of DynamoDbClient or a configuration array containing key, secret, region'); } - $this->client = DynamoDbClient::factory(array( - 'key' => $connection["key"], - 'secret' => $connection["secret"], - 'region' =>$connection["region"] + $this->client = new DynamoDbClient(array( + 'credentials' => array( + 'key' => $connection["key"], + 'secret' => $connection["secret"], + ), + 'region' => $connection["region"], + 'version' => 'latest', )); } else { $this->client = $connection; } + $this->marshaler = new Marshaler(); + $this->config = array_merge(array( 'client_table' => 'oauth_clients', 'access_token_table' => 'oauth_access_tokens', @@ -84,7 +91,7 @@ public function checkClientCredentials($client_id, $client_secret = null) "Key" => array('client_id' => array('S' => $client_id)) )); - return $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret; + return isset($result["Item"]) && $result["Item"]["client_secret"]["S"] == $client_secret; } public function isPublicClient($client_id) @@ -94,7 +101,7 @@ public function isPublicClient($client_id) "Key" => array('client_id' => array('S' => $client_id)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } @@ -108,7 +115,7 @@ public function getClientDetails($client_id) "TableName"=> $this->config['client_table'], "Key" => array('client_id' => array('S' => $client_id)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $result = $this->dynamo2array($result); @@ -124,11 +131,11 @@ public function getClientDetails($client_id) public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); - $clientData = array_filter($clientData, 'self::isNotEmpty'); + $clientData = array_filter($clientData, self::isNotEmpty(...)); - $result = $this->client->putItem(array( + $this->client->putItem(array( 'TableName' => $this->config['client_table'], - 'Item' => $this->client->formatAttributes($clientData) + 'Item' => $this->marshaler->marshalItem($clientData) )); return true; @@ -154,7 +161,7 @@ public function getAccessToken($access_token) "TableName"=> $this->config['access_token_table'], "Key" => array('access_token' => array('S' => $access_token)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $token = $this->dynamo2array($result); @@ -171,11 +178,11 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope'); - $clientData = array_filter($clientData, 'self::isNotEmpty'); + $clientData = array_filter($clientData, self::isNotEmpty(...)); - $result = $this->client->putItem(array( + $this->client->putItem(array( 'TableName' => $this->config['access_token_table'], - 'Item' => $this->client->formatAttributes($clientData) + 'Item' => $this->marshaler->marshalItem($clientData) )); return true; @@ -186,11 +193,11 @@ public function unsetAccessToken($access_token) { $result = $this->client->deleteItem(array( 'TableName' => $this->config['access_token_table'], - 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)), + 'Key' => array('access_token' => array('S' => $access_token)), 'ReturnValues' => 'ALL_OLD', )); - return null !== $result->get('Attributes'); + return !empty($result['Attributes']); } /* OAuth2\Storage\AuthorizationCodeInterface */ @@ -200,7 +207,7 @@ public function getAuthorizationCode($code) "TableName"=> $this->config['code_table'], "Key" => array('authorization_code' => array('S' => $code)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $token = $this->dynamo2array($result); @@ -219,11 +226,11 @@ public function setAuthorizationCode($authorization_code, $client_id, $user_id, $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'); - $clientData = array_filter($clientData, 'self::isNotEmpty'); + $clientData = array_filter($clientData, self::isNotEmpty(...)); - $result = $this->client->putItem(array( + $this->client->putItem(array( 'TableName' => $this->config['code_table'], - 'Item' => $this->client->formatAttributes($clientData) + 'Item' => $this->marshaler->marshalItem($clientData) )); return true; @@ -232,9 +239,9 @@ public function setAuthorizationCode($authorization_code, $client_id, $user_id, public function expireAuthorizationCode($code) { - $result = $this->client->deleteItem(array( + $this->client->deleteItem(array( 'TableName' => $this->config['code_table'], - 'Key' => $this->client->formatAttributes(array("authorization_code" => $code)) + 'Key' => array('authorization_code' => array('S' => $code)) )); return true; @@ -305,7 +312,7 @@ public function getRefreshToken($refresh_token) "TableName"=> $this->config['refresh_token_table'], "Key" => array('refresh_token' => array('S' => $refresh_token)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $token = $this->dynamo2array($result); @@ -320,11 +327,11 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); - $clientData = array_filter($clientData, 'self::isNotEmpty'); + $clientData = array_filter($clientData, self::isNotEmpty(...)); - $result = $this->client->putItem(array( + $this->client->putItem(array( 'TableName' => $this->config['refresh_token_table'], - 'Item' => $this->client->formatAttributes($clientData) + 'Item' => $this->marshaler->marshalItem($clientData) )); return true; @@ -332,9 +339,9 @@ public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, public function unsetRefreshToken($refresh_token) { - $result = $this->client->deleteItem(array( + $this->client->deleteItem(array( 'TableName' => $this->config['refresh_token_table'], - 'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token)) + 'Key' => array('refresh_token' => array('S' => $refresh_token)) )); return true; @@ -358,7 +365,7 @@ public function getUser($username) "TableName"=> $this->config['user_table'], "Key" => array('username' => array('S' => $username)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $token = $this->dynamo2array($result); @@ -373,11 +380,11 @@ public function setUser($username, $password, $first_name = null, $last_name = n $password = $this->hashPassword($password); $clientData = compact('username', 'password', 'first_name', 'last_name'); - $clientData = array_filter($clientData, 'self::isNotEmpty'); + $clientData = array_filter($clientData, self::isNotEmpty(...)); - $result = $this->client->putItem(array( + $this->client->putItem(array( 'TableName' => $this->config['user_table'], - 'Item' => $this->client->formatAttributes($clientData) + 'Item' => $this->marshaler->marshalItem($clientData) )); return true; @@ -388,7 +395,6 @@ public function setUser($username, $password, $first_name = null, $last_name = n public function scopeExists($scope) { $scope = explode(' ', $scope); - $scope_query = array(); $count = 0; foreach ($scope as $key => $val) { $result = $this->client->query(array( @@ -422,9 +428,8 @@ public function getDefaultScope($client_id = null) ) )); $defaultScope = array(); - if ($result->count() > 0) { - $array = $result->toArray(); - foreach ($array["Items"] as $item) { + if (($result['Count'] ?? 0) > 0) { + foreach ($result["Items"] as $item) { $defaultScope[] = $item['scope']['S']; } @@ -441,7 +446,7 @@ public function getClientKey($client_id, $subject) "TableName"=> $this->config['jwt_table'], "Key" => array('client_id' => array('S' => $client_id), 'subject' => array('S' => $subject)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $token = $this->dynamo2array($result); @@ -475,12 +480,12 @@ public function setJti($client_id, $subject, $audience, $expires, $jti) /* PublicKeyInterface */ public function getPublicKey($client_id = '0') { - + $client_id = $client_id ?? '0'; $result = $this->client->getItem(array( "TableName"=> $this->config['public_key_table'], "Key" => array('client_id' => array('S' => $client_id)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $token = $this->dynamo2array($result); @@ -491,11 +496,12 @@ public function getPublicKey($client_id = '0') public function getPrivateKey($client_id = '0') { + $client_id = $client_id ?? '0'; $result = $this->client->getItem(array( "TableName"=> $this->config['public_key_table'], "Key" => array('client_id' => array('S' => $client_id)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return false ; } $token = $this->dynamo2array($result); @@ -505,11 +511,12 @@ public function getPrivateKey($client_id = '0') public function getEncryptionAlgorithm($client_id = null) { + $client_id = $client_id ?? '0'; $result = $this->client->getItem(array( "TableName"=> $this->config['public_key_table'], "Key" => array('client_id' => array('S' => $client_id)) )); - if ($result->count()==0) { + if (!isset($result["Item"])) { return 'RS256' ; } $token = $this->dynamo2array($result); @@ -520,14 +527,13 @@ public function getEncryptionAlgorithm($client_id = null) /** * Transform dynamodb resultset to an array. * @param $dynamodbResult - * @return $array + * @return array */ private function dynamo2array($dynamodbResult) { $result = array(); foreach ($dynamodbResult["Item"] as $key => $val) { $result[$key] = $val["S"]; - $result[] = $val["S"]; } return $result; @@ -537,4 +543,4 @@ private static function isNotEmpty($value) { return null !== $value && '' !== $value; } -} \ No newline at end of file +} diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index c33bd0ebb..b059ce200 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -112,7 +112,7 @@ public function setUser($username, $password, $firstName = null, $lastName = nul public function getUserDetails($username) { - if (!isset($this->userCredentials[$username])) { + if (is_null($username) || !isset($this->userCredentials[$username])) { return false; } @@ -339,7 +339,7 @@ public function setJti($client_id, $subject, $audience, $expires, $jti) /*PublicKeyInterface */ public function getPublicKey($client_id = null) { - if (isset($this->keys[$client_id])) { + if (!is_null($client_id) && isset($this->keys[$client_id])) { return $this->keys[$client_id]['public_key']; } @@ -353,7 +353,7 @@ public function getPublicKey($client_id = null) public function getPrivateKey($client_id = null) { - if (isset($this->keys[$client_id])) { + if (!is_null($client_id) && isset($this->keys[$client_id])) { return $this->keys[$client_id]['private_key']; } @@ -367,7 +367,7 @@ public function getPrivateKey($client_id = null) public function getEncryptionAlgorithm($client_id = null) { - if (isset($this->keys[$client_id]['encryption_algorithm'])) { + if (!is_null($client_id) && isset($this->keys[$client_id]['encryption_algorithm'])) { return $this->keys[$client_id]['encryption_algorithm']; } diff --git a/src/OAuth2/Storage/Mongo.php b/src/OAuth2/Storage/Mongo.php deleted file mode 100644 index 92f93d5b2..000000000 --- a/src/OAuth2/Storage/Mongo.php +++ /dev/null @@ -1,396 +0,0 @@ - - */ -class Mongo implements AuthorizationCodeInterface, - AccessTokenInterface, - ClientCredentialsInterface, - UserCredentialsInterface, - RefreshTokenInterface, - JwtBearerInterface, - PublicKeyInterface, - OpenIDAuthorizationCodeInterface -{ - protected $db; - protected $config; - - public function __construct($connection, $config = array()) - { - if ($connection instanceof \MongoDB) { - $this->db = $connection; - } else { - if (!is_array($connection)) { - throw new \InvalidArgumentException('First argument to OAuth2\Storage\Mongo must be an instance of MongoDB or a configuration array'); - } - $server = sprintf('mongodb://%s:%d', $connection['host'], $connection['port']); - $m = new \MongoClient($server); - $this->db = $m->{$connection['database']}; - } - - $this->config = array_merge(array( - 'client_table' => 'oauth_clients', - 'access_token_table' => 'oauth_access_tokens', - 'refresh_token_table' => 'oauth_refresh_tokens', - 'code_table' => 'oauth_authorization_codes', - 'user_table' => 'oauth_users', - 'key_table' => 'oauth_keys', - 'jwt_table' => 'oauth_jwt', - ), $config); - } - - // Helper function to access a MongoDB collection by `type`: - protected function collection($name) - { - return $this->db->{$this->config[$name]}; - } - - /* ClientCredentialsInterface */ - public function checkClientCredentials($client_id, $client_secret = null) - { - if ($result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { - return $result['client_secret'] == $client_secret; - } - - return false; - } - - public function isPublicClient($client_id) - { - if (!$result = $this->collection('client_table')->findOne(array('client_id' => $client_id))) { - return false; - } - - return empty($result['client_secret']); - } - - /* ClientInterface */ - public function getClientDetails($client_id) - { - $result = $this->collection('client_table')->findOne(array('client_id' => $client_id)); - - return is_null($result) ? false : $result; - } - - public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) - { - if ($this->getClientDetails($client_id)) { - $this->collection('client_table')->update( - array('client_id' => $client_id), - array('$set' => array( - 'client_secret' => $client_secret, - 'redirect_uri' => $redirect_uri, - 'grant_types' => $grant_types, - 'scope' => $scope, - 'user_id' => $user_id, - )) - ); - } else { - $client = array( - 'client_id' => $client_id, - 'client_secret' => $client_secret, - 'redirect_uri' => $redirect_uri, - 'grant_types' => $grant_types, - 'scope' => $scope, - 'user_id' => $user_id, - ); - $this->collection('client_table')->insert($client); - } - - return true; - } - - public function checkRestrictedGrantType($client_id, $grant_type) - { - $details = $this->getClientDetails($client_id); - if (isset($details['grant_types'])) { - $grant_types = explode(' ', $details['grant_types']); - - return in_array($grant_type, $grant_types); - } - - // if grant_types are not defined, then none are restricted - return true; - } - - /* AccessTokenInterface */ - public function getAccessToken($access_token) - { - $token = $this->collection('access_token_table')->findOne(array('access_token' => $access_token)); - - return is_null($token) ? false : $token; - } - - public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) - { - // if it exists, update it. - if ($this->getAccessToken($access_token)) { - $this->collection('access_token_table')->update( - array('access_token' => $access_token), - array('$set' => array( - 'client_id' => $client_id, - 'expires' => $expires, - 'user_id' => $user_id, - 'scope' => $scope - )) - ); - } else { - $token = array( - 'access_token' => $access_token, - 'client_id' => $client_id, - 'expires' => $expires, - 'user_id' => $user_id, - 'scope' => $scope - ); - $this->collection('access_token_table')->insert($token); - } - - return true; - } - - public function unsetAccessToken($access_token) - { - $result = $this->collection('access_token_table')->remove(array( - 'access_token' => $access_token - ), array('w' => 1)); - - return $result['n'] > 0; - } - - - /* AuthorizationCodeInterface */ - public function getAuthorizationCode($code) - { - $code = $this->collection('code_table')->findOne(array('authorization_code' => $code)); - - return is_null($code) ? false : $code; - } - - public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null) - { - // if it exists, update it. - if ($this->getAuthorizationCode($code)) { - $this->collection('code_table')->update( - array('authorization_code' => $code), - array('$set' => array( - 'client_id' => $client_id, - 'user_id' => $user_id, - 'redirect_uri' => $redirect_uri, - 'expires' => $expires, - 'scope' => $scope, - 'id_token' => $id_token, - 'code_challenge' => $code_challenge, - 'code_challenge_method' => $code_challenge_method, - )) - ); - } else { - $token = array( - 'authorization_code' => $code, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'redirect_uri' => $redirect_uri, - 'expires' => $expires, - 'scope' => $scope, - 'id_token' => $id_token, - 'code_challenge' => $code_challenge, - 'code_challenge_method' => $code_challenge_method, - ); - $this->collection('code_table')->insert($token); - } - - return true; - } - - public function expireAuthorizationCode($code) - { - $this->collection('code_table')->remove(array('authorization_code' => $code)); - - return true; - } - - /* UserCredentialsInterface */ - public function checkUserCredentials($username, $password) - { - if ($user = $this->getUser($username)) { - return $this->checkPassword($user, $password); - } - - return false; - } - - public function getUserDetails($username) - { - if ($user = $this->getUser($username)) { - $user['user_id'] = $user['username']; - } - - return $user; - } - - /* RefreshTokenInterface */ - public function getRefreshToken($refresh_token) - { - $token = $this->collection('refresh_token_table')->findOne(array('refresh_token' => $refresh_token)); - - return is_null($token) ? false : $token; - } - - public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) - { - $token = array( - 'refresh_token' => $refresh_token, - 'client_id' => $client_id, - 'user_id' => $user_id, - 'expires' => $expires, - 'scope' => $scope - ); - $this->collection('refresh_token_table')->insert($token); - - return true; - } - - public function unsetRefreshToken($refresh_token) - { - $result = $this->collection('refresh_token_table')->remove(array( - 'refresh_token' => $refresh_token - ), array('w' => 1)); - - return $result['n'] > 0; - } - - // plaintext passwords are bad! Override this for your application - protected function checkPassword($user, $password) - { - return $user['password'] == $password; - } - - public function getUser($username) - { - $result = $this->collection('user_table')->findOne(array('username' => $username)); - - return is_null($result) ? false : $result; - } - - public function setUser($username, $password, $firstName = null, $lastName = null) - { - if ($this->getUser($username)) { - $this->collection('user_table')->update( - array('username' => $username), - array('$set' => array( - 'password' => $password, - 'first_name' => $firstName, - 'last_name' => $lastName - )) - ); - } else { - $user = array( - 'username' => $username, - 'password' => $password, - 'first_name' => $firstName, - 'last_name' => $lastName - ); - $this->collection('user_table')->insert($user); - } - - return true; - } - - public function getClientKey($client_id, $subject) - { - $result = $this->collection('jwt_table')->findOne(array( - 'client_id' => $client_id, - 'subject' => $subject - )); - - return is_null($result) ? false : $result['key']; - } - - public function getClientScope($client_id) - { - if (!$clientDetails = $this->getClientDetails($client_id)) { - return false; - } - - if (isset($clientDetails['scope'])) { - return $clientDetails['scope']; - } - - return null; - } - - public function getJti($client_id, $subject, $audience, $expiration, $jti) - { - //TODO: Needs mongodb implementation. - throw new \Exception('getJti() for the MongoDB driver is currently unimplemented.'); - } - - public function setJti($client_id, $subject, $audience, $expiration, $jti) - { - //TODO: Needs mongodb implementation. - throw new \Exception('setJti() for the MongoDB driver is currently unimplemented.'); - } - - public function getPublicKey($client_id = null) - { - if ($client_id) { - $result = $this->collection('key_table')->findOne(array( - 'client_id' => $client_id - )); - if ($result) { - return $result['public_key']; - } - } - - $result = $this->collection('key_table')->findOne(array( - 'client_id' => null - )); - return is_null($result) ? false : $result['public_key']; - } - - public function getPrivateKey($client_id = null) - { - if ($client_id) { - $result = $this->collection('key_table')->findOne(array( - 'client_id' => $client_id - )); - if ($result) { - return $result['private_key']; - } - } - - $result = $this->collection('key_table')->findOne(array( - 'client_id' => null - )); - return is_null($result) ? false : $result['private_key']; - } - - public function getEncryptionAlgorithm($client_id = null) - { - if ($client_id) { - $result = $this->collection('key_table')->findOne(array( - 'client_id' => $client_id - )); - if ($result) { - return $result['encryption_algorithm']; - } - } - - $result = $this->collection('key_table')->findOne(array( - 'client_id' => null - )); - return is_null($result) ? 'RS256' : $result['encryption_algorithm']; - } -} diff --git a/src/OAuth2/Storage/Redis.php b/src/OAuth2/Storage/Redis.php index 5a41dfc22..9b74798d4 100644 --- a/src/OAuth2/Storage/Redis.php +++ b/src/OAuth2/Storage/Redis.php @@ -79,7 +79,11 @@ protected function setValue($key, $value, $expire=0) // check that the key was set properly // if this fails, an exception will usually thrown, so this step isn't strictly necessary - return is_bool($ret) ? $ret : $ret->getPayload() == 'OK'; + if (is_bool($ret)) { + return $ret; + } + + return (string) $ret === 'OK'; } protected function expireValue($key) diff --git a/stubs/couchbase.stub b/stubs/couchbase.stub new file mode 100644 index 000000000..afb2034e1 --- /dev/null +++ b/stubs/couchbase.stub @@ -0,0 +1,47 @@ +getMemoryStorage(); $controller = new AuthorizeController($storage); + $this->assertInstanceOf('OAuth2\Controller\AuthorizeController', $controller); } private function getTestServer($config = array()) diff --git a/test/OAuth2/Controller/ResourceControllerTest.php b/test/OAuth2/Controller/ResourceControllerTest.php index cd54d239a..5e362b564 100644 --- a/test/OAuth2/Controller/ResourceControllerTest.php +++ b/test/OAuth2/Controller/ResourceControllerTest.php @@ -162,6 +162,7 @@ public function testCreateController() $storage = Bootstrap::getInstance()->getMemoryStorage(); $tokenType = new \OAuth2\TokenType\Bearer(); $controller = new ResourceController($tokenType, $storage); + $this->assertInstanceOf('OAuth2\Controller\ResourceController', $controller); } private function getTestServer($config = array()) diff --git a/test/OAuth2/Controller/TokenControllerTest.php b/test/OAuth2/Controller/TokenControllerTest.php index d18eaa6d7..191e540e7 100644 --- a/test/OAuth2/Controller/TokenControllerTest.php +++ b/test/OAuth2/Controller/TokenControllerTest.php @@ -319,6 +319,7 @@ public function testCreateController() $storage = Bootstrap::getInstance()->getMemoryStorage(); $accessToken = new \OAuth2\ResponseType\AccessToken($storage); $controller = new TokenController($accessToken, $storage); + $this->assertInstanceOf('OAuth2\Controller\TokenController', $controller); } private function getTestServer() diff --git a/test/OAuth2/Encryption/FirebaseJwtTest.php b/test/OAuth2/Encryption/FirebaseJwtTest.php index 63a5d4036..0512c16dc 100644 --- a/test/OAuth2/Encryption/FirebaseJwtTest.php +++ b/test/OAuth2/Encryption/FirebaseJwtTest.php @@ -3,6 +3,7 @@ namespace OAuth2\Encryption; use OAuth2\Storage\Bootstrap; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class FirebaseJwtTest extends TestCase @@ -12,25 +13,38 @@ class FirebaseJwtTest extends TestCase public function setUp(): void { $this->privateKey = <<assertFalse($jwtUtil->decode('go.o.b')); } - /** @dataProvider provideClientCredentials */ + #[DataProvider('provideClientCredentials')] public function testInvalidJwtHeader($client_id, $client_key) { $jwtUtil = new FirebaseJwt(); @@ -90,7 +104,7 @@ public function testInvalidJwtHeader($client_id, $client_key) $this->assertFalse($payload); } - public function provideClientCredentials() + public static function provideClientCredentials() { $storage = Bootstrap::getInstance()->getMemoryStorage(); $client_id = 'Test Client ID'; diff --git a/test/OAuth2/Encryption/JwtTest.php b/test/OAuth2/Encryption/JwtTest.php index 376a922b1..60ce8def8 100644 --- a/test/OAuth2/Encryption/JwtTest.php +++ b/test/OAuth2/Encryption/JwtTest.php @@ -3,6 +3,7 @@ namespace OAuth2\Encryption; use OAuth2\Storage\Bootstrap; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class JwtTest extends TestCase @@ -12,25 +13,38 @@ class JwtTest extends TestCase public function setUp(): void { $this->privateKey = <<assertFalse($jwtUtil->decode('go.o.b')); } - /** @dataProvider provideClientCredentials */ + #[DataProvider('provideClientCredentials')] public function testInvalidJwtHeader($client_id, $client_key) { $jwtUtil = new Jwt(); @@ -90,7 +104,7 @@ public function testInvalidJwtHeader($client_id, $client_key) $this->assertFalse($payload); } - public function provideClientCredentials() + public static function provideClientCredentials() { $storage = Bootstrap::getInstance()->getMemoryStorage(); $client_id = 'Test Client ID'; diff --git a/test/OAuth2/GrantType/JwtBearerTest.php b/test/OAuth2/GrantType/JwtBearerTest.php index 4f6d67b2c..1c24a26c8 100644 --- a/test/OAuth2/GrantType/JwtBearerTest.php +++ b/test/OAuth2/GrantType/JwtBearerTest.php @@ -16,21 +16,34 @@ class JwtBearerTest extends TestCase public function setUp(): void { $this->privateKey = <<assertEquals($code['id_token'], $new_id_token); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testRemoveIdTokenFromAuthorizationCode($storage) { // add new code diff --git a/test/OAuth2/OpenID/Storage/UserClaimsTest.php b/test/OAuth2/OpenID/Storage/UserClaimsTest.php index 840f6c566..da79ac5d5 100644 --- a/test/OAuth2/OpenID/Storage/UserClaimsTest.php +++ b/test/OAuth2/OpenID/Storage/UserClaimsTest.php @@ -4,10 +4,11 @@ use OAuth2\Storage\BaseTest; use OAuth2\Storage\NullStorage; +use PHPUnit\Framework\Attributes\DataProvider; class UserClaimsTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testGetUserClaims($storage) { if ($storage instanceof NullStorage) { @@ -17,7 +18,8 @@ public function testGetUserClaims($storage) } if (!$storage instanceof UserClaimsInterface) { - // incompatible storage + $this->markTestSkipped('Incompatible storage: UserClaimsInterface required'); + return; } diff --git a/test/OAuth2/ServerTest.php b/test/OAuth2/ServerTest.php index fab526a6f..36ffd1af7 100644 --- a/test/OAuth2/ServerTest.php +++ b/test/OAuth2/ServerTest.php @@ -6,11 +6,9 @@ use OAuth2\ResponseType\AuthorizationCode; use OAuth2\Storage\Bootstrap; use PHPUnit\Framework\TestCase; -use Yoast\PHPUnitPolyfills\Polyfills\ExpectPHPException; class ServerTest extends TestCase { - use ExpectPHPException; public function testGetAuthorizeControllerWithNoClientStorageThrowsException() { @@ -108,7 +106,7 @@ public function testGetTokenControllerWithAccessTokenAndClientCredentialsStorage $server = new Server(); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientCredentialsInterface')); - $server->getTokenController(); + $this->assertNotNull($server->getTokenController()); } public function testGetTokenControllerAccessTokenStorageAndClientCredentialsStorageAndGrantTypes() @@ -117,7 +115,7 @@ public function testGetTokenControllerAccessTokenStorageAndClientCredentialsStor $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientCredentialsInterface')); $server->addGrantType($this->createMock('OAuth2\GrantType\AuthorizationCode')); - $server->getTokenController(); + $this->assertNotNull($server->getTokenController()); } public function testGetResourceControllerWithNoAccessTokenStorageThrowsException() @@ -131,7 +129,7 @@ public function testGetResourceControllerWithAccessTokenStorage() { $server = new Server(); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface')); - $server->getResourceController(); + $this->assertNotNull($server->getResourceController()); } public function testAddingStorageWithInvalidClass() @@ -162,7 +160,7 @@ public function testAddingStorageWithValidKeyOnlySetsThatKey() $reflection = new \ReflectionClass($server); $prop = $reflection->getProperty('storages'); - $prop->setAccessible(true); + $storages = $prop->getValue($server); // get the private "storages" property @@ -252,11 +250,11 @@ public function testAddingResponseType() $storage ->expects($this->any()) ->method('getClientDetails') - ->will($this->returnValue(array('client_id' => 'some_client'))); + ->willReturn(array('client_id' => 'some_client')); $storage ->expects($this->any()) ->method('checkRestrictedGrantType') - ->will($this->returnValue(true)); + ->willReturn(true); // add with the "code" key explicitly set $codeType = new AuthorizationCode($storage); @@ -309,11 +307,11 @@ public function testCustomClientAssertionType() $clientAssertionType ->expects($this->once()) ->method('validateRequest') - ->will($this->returnValue(true)); + ->willReturn(true); $clientAssertionType ->expects($this->once()) ->method('getClientId') - ->will($this->returnValue('Test Client ID')); + ->willReturn('Test Client ID'); // create mock storage $storage = Bootstrap::getInstance()->getMemoryStorage(); @@ -334,7 +332,7 @@ public function testHttpBasicConfig() $reflection = new \ReflectionClass($httpBasic); $prop = $reflection->getProperty('config'); - $prop->setAccessible(true); + $config = $prop->getValue($httpBasic); // get the private "config" property @@ -357,11 +355,11 @@ public function testRefreshTokenConfig() $reflection1 = new \ReflectionClass($refreshToken1); $prop1 = $reflection1->getProperty('config'); - $prop1->setAccessible(true); + $reflection2 = new \ReflectionClass($refreshToken2); $prop2 = $reflection2->getProperty('config'); - $prop2->setAccessible(true); + // get the private "config" property $config1 = $prop1->getValue($refreshToken1); @@ -503,7 +501,7 @@ public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay() public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrowsException() { - $this->expectErrorMessage('OAuth2\ResponseType\AccessTokenInterface'); + $this->expectExceptionMessage('OAuth2\ResponseType\AccessTokenInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface'); @@ -581,7 +579,7 @@ public function testUsingOpenIDConnectWithAuthorizationCodeStorageThrowsExceptio $token = $this->createMock('OAuth2\Storage\AccessTokenInterface'); $authcode = $this->createMock('OAuth2\Storage\AuthorizationCodeInterface'); - $this->expectErrorMessage('OAuth2\OpenID\Storage\AuthorizationCodeInterface'); + $this->expectExceptionMessage('OAuth2\OpenID\Storage\AuthorizationCodeInterface'); $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array( 'use_openid_connect' => true, 'issuer' => 'someguy' diff --git a/test/OAuth2/Storage/AccessTokenTest.php b/test/OAuth2/Storage/AccessTokenTest.php index b34e0bfc0..9fccee844 100644 --- a/test/OAuth2/Storage/AccessTokenTest.php +++ b/test/OAuth2/Storage/AccessTokenTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class AccessTokenTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testSetAccessToken(AccessTokenInterface $storage) { if ($storage instanceof NullStorage) { @@ -55,15 +57,21 @@ public function testSetAccessToken(AccessTokenInterface $storage) $this->assertTrue($success); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testUnsetAccessToken(AccessTokenInterface $storage) { - if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { + if ($storage instanceof NullStorage) { $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); return; } + if (!method_exists($storage, 'unsetAccessToken')) { + $this->markTestSkipped('Skipped Storage: unsetAccessToken not implemented'); + + return; + } + // assert token we are about to unset does not exist $token = $storage->getAccessToken('revokabletoken'); $this->assertFalse($token); @@ -82,15 +90,21 @@ public function testUnsetAccessToken(AccessTokenInterface $storage) $this->assertFalse($token); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testUnsetAccessTokenReturnsFalse(AccessTokenInterface $storage) { - if ($storage instanceof NullStorage || !method_exists($storage, 'unsetAccessToken')) { + if ($storage instanceof NullStorage) { $this->markTestSkipped('Skipped Storage: ' . $storage->getMessage()); return; } + if (!method_exists($storage, 'unsetAccessToken')) { + $this->markTestSkipped('Skipped Storage: unsetAccessToken not implemented'); + + return; + } + // assert token we are about to unset does not exist $token = $storage->getAccessToken('nonexistanttoken'); $this->assertFalse($token); diff --git a/test/OAuth2/Storage/AuthorizationCodeTest.php b/test/OAuth2/Storage/AuthorizationCodeTest.php index 2d901b501..9fb015387 100644 --- a/test/OAuth2/Storage/AuthorizationCodeTest.php +++ b/test/OAuth2/Storage/AuthorizationCodeTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class AuthorizationCodeTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testGetAuthorizationCode(AuthorizationCodeInterface $storage) { if ($storage instanceof NullStorage) { @@ -22,7 +24,7 @@ public function testGetAuthorizationCode(AuthorizationCodeInterface $storage) $this->assertNotNull($details); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testSetAuthorizationCode(AuthorizationCodeInterface $storage) { if ($storage instanceof NullStorage) { @@ -77,7 +79,7 @@ public function testSetAuthorizationCode(AuthorizationCodeInterface $storage) $this->assertTrue($success); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testExpireAccessToken(AccessTokenInterface $storage) { if ($storage instanceof NullStorage) { diff --git a/test/OAuth2/Storage/ClientCredentialsTest.php b/test/OAuth2/Storage/ClientCredentialsTest.php index 15289af30..af8f973c4 100644 --- a/test/OAuth2/Storage/ClientCredentialsTest.php +++ b/test/OAuth2/Storage/ClientCredentialsTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class ClientCredentialsTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testCheckClientCredentials(ClientCredentialsInterface $storage) { if ($storage instanceof NullStorage) { diff --git a/test/OAuth2/Storage/ClientTest.php b/test/OAuth2/Storage/ClientTest.php index 6a5cc0b49..2a97ca739 100644 --- a/test/OAuth2/Storage/ClientTest.php +++ b/test/OAuth2/Storage/ClientTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class ClientTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testGetClientDetails(ClientInterface $storage) { if ($storage instanceof NullStorage) { @@ -25,7 +27,7 @@ public function testGetClientDetails(ClientInterface $storage) $this->assertArrayHasKey('redirect_uri', $details); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testCheckRestrictedGrantType(ClientInterface $storage) { if ($storage instanceof NullStorage) { @@ -43,7 +45,7 @@ public function testCheckRestrictedGrantType(ClientInterface $storage) $this->assertTrue($pass); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testGetAccessToken(ClientInterface $storage) { if ($storage instanceof NullStorage) { @@ -61,7 +63,7 @@ public function testGetAccessToken(ClientInterface $storage) $this->assertNotNull($details); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testIsPublicClient(ClientInterface $storage) { if ($storage instanceof NullStorage) { @@ -84,7 +86,7 @@ public function testIsPublicClient(ClientInterface $storage) $this->assertFalse($storage->isPublicClient($confidentialClientId)); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testSaveClient(ClientInterface $storage) { if ($storage instanceof NullStorage) { diff --git a/test/OAuth2/Storage/DynamoDBTest.php b/test/OAuth2/Storage/DynamoDBTest.php index 2147f0914..cc58345b3 100644 --- a/test/OAuth2/Storage/DynamoDBTest.php +++ b/test/OAuth2/Storage/DynamoDBTest.php @@ -2,37 +2,27 @@ namespace OAuth2\Storage; +use Aws\Result; + class DynamoDBTest extends BaseTest { public function testGetDefaultScope() { $client = $this->getMockBuilder('\Aws\DynamoDb\DynamoDbClient') ->disableOriginalConstructor() - ->setMethods(array('query')) - ->getMock(); - - $return = $this->getMockBuilder('\Guzzle\Service\Resource\Model') - ->setMethods(array('count', 'toArray')) + ->addMethods(array('query')) ->getMock(); - $data = array( + $data = new Result(array( 'Items' => array(), 'Count' => 0, 'ScannedCount'=> 0 - ); - - $return->expects($this->once()) - ->method('count') - ->will($this->returnValue(count($data))); - - $return->expects($this->once()) - ->method('toArray') - ->will($this->returnValue($data)); + )); // should return null default scope if none is set in database $client->expects($this->once()) ->method('query') - ->will($this->returnValue($return)); + ->willReturn($data); $storage = new DynamoDB($client); $this->assertNull($storage->getDefaultScope()); diff --git a/test/OAuth2/Storage/JwtAccessTokenTest.php b/test/OAuth2/Storage/JwtAccessTokenTest.php index a6acbea1f..afa460c3f 100644 --- a/test/OAuth2/Storage/JwtAccessTokenTest.php +++ b/test/OAuth2/Storage/JwtAccessTokenTest.php @@ -3,14 +3,22 @@ namespace OAuth2\Storage; use OAuth2\Encryption\Jwt; +use PHPUnit\Framework\Attributes\DataProvider; class JwtAccessTokenTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testSetAccessToken($storage) { - if (!$storage instanceof PublicKey) { - // incompatible storage + if ($storage instanceof NullStorage) { + $this->markTestSkipped("Skipped Storage: {$storage}"); + + return; + } + + if (!$storage instanceof PublicKeyInterface) { + $this->markTestSkipped('Incompatible storage: PublicKeyInterface required'); + return; } diff --git a/test/OAuth2/Storage/JwtBearerTest.php b/test/OAuth2/Storage/JwtBearerTest.php index d0ab9b899..880b3b650 100644 --- a/test/OAuth2/Storage/JwtBearerTest.php +++ b/test/OAuth2/Storage/JwtBearerTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class JwtBearerTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testGetClientKey(JwtBearerInterface $storage) { if ($storage instanceof NullStorage) { diff --git a/test/OAuth2/Storage/PdoTest.php b/test/OAuth2/Storage/PdoTest.php index 9a7630423..e269bc2d0 100644 --- a/test/OAuth2/Storage/PdoTest.php +++ b/test/OAuth2/Storage/PdoTest.php @@ -2,11 +2,8 @@ namespace OAuth2\Storage; -use Yoast\PHPUnitPolyfills\Polyfills\ExpectPHPException; - class PdoTest extends BaseTest { - use ExpectPHPException; public function testCreatePdoStorageUsingPdoClass() { @@ -36,7 +33,7 @@ public function testCreatePdoStorageUsingConfig() public function testCreatePdoStorageWithoutDSNThrowsException() { - $this->expectErrorMessage('dsn'); + $this->expectExceptionMessage('dsn'); $config = array('username' => 'brent', 'password' => 'brentisaballer'); $storage = new Pdo($config); } diff --git a/test/OAuth2/Storage/PublicKeyTest.php b/test/OAuth2/Storage/PublicKeyTest.php index f85195870..db2ff9c3d 100644 --- a/test/OAuth2/Storage/PublicKeyTest.php +++ b/test/OAuth2/Storage/PublicKeyTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class PublicKeyTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testSetAccessToken($storage) { if ($storage instanceof NullStorage) { @@ -14,7 +16,8 @@ public function testSetAccessToken($storage) } if (!$storage instanceof PublicKeyInterface) { - // incompatible storage + $this->markTestSkipped('Incompatible storage: PublicKeyInterface required'); + return; } diff --git a/test/OAuth2/Storage/RefreshTokenTest.php b/test/OAuth2/Storage/RefreshTokenTest.php index 314c93195..e4172d5ff 100644 --- a/test/OAuth2/Storage/RefreshTokenTest.php +++ b/test/OAuth2/Storage/RefreshTokenTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class RefreshTokenTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testSetRefreshToken(RefreshTokenInterface $storage) { if ($storage instanceof NullStorage) { diff --git a/test/OAuth2/Storage/ScopeTest.php b/test/OAuth2/Storage/ScopeTest.php index fd1edeb93..97f1faa33 100644 --- a/test/OAuth2/Storage/ScopeTest.php +++ b/test/OAuth2/Storage/ScopeTest.php @@ -3,10 +3,11 @@ namespace OAuth2\Storage; use OAuth2\Scope; +use PHPUnit\Framework\Attributes\DataProvider; class ScopeTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testScopeExists($storage) { if ($storage instanceof NullStorage) { @@ -16,7 +17,8 @@ public function testScopeExists($storage) } if (!$storage instanceof ScopeInterface) { - // incompatible storage + $this->markTestSkipped('Incompatible storage: ScopeInterface required'); + return; } @@ -28,7 +30,7 @@ public function testScopeExists($storage) $this->assertFalse($scopeUtil->scopeExists('supportedscope1 supportedscope2 supportedscope3 fakescope')); } - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testGetDefaultScope($storage) { if ($storage instanceof NullStorage) { @@ -38,7 +40,8 @@ public function testGetDefaultScope($storage) } if (!$storage instanceof ScopeInterface) { - // incompatible storage + $this->markTestSkipped('Incompatible storage: ScopeInterface required'); + return; } diff --git a/test/OAuth2/Storage/UserCredentialsTest.php b/test/OAuth2/Storage/UserCredentialsTest.php index 65655a6b2..fad3b15f4 100644 --- a/test/OAuth2/Storage/UserCredentialsTest.php +++ b/test/OAuth2/Storage/UserCredentialsTest.php @@ -2,9 +2,11 @@ namespace OAuth2\Storage; +use PHPUnit\Framework\Attributes\DataProvider; + class UserCredentialsTest extends BaseTest { - /** @dataProvider provideStorage */ + #[DataProvider('provideStorage')] public function testCheckUserCredentials(UserCredentialsInterface $storage) { if ($storage instanceof NullStorage) { diff --git a/test/config/storage.json b/test/config/storage.json index 52d3f2399..c56118a56 100644 --- a/test/config/storage.json +++ b/test/config/storage.json @@ -140,7 +140,7 @@ }, "jwt": { "Test Client ID": { - "key": "-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC5/SxVlE8gnpFqCxgl2wjhzY7u\ncEi00s0kUg3xp7lVEvgLgYcAnHiWp+gtSjOFfH2zsvpiWm6Lz5f743j/FEzHIO1o\nwR0p4d9pOaJK07d01+RzoQLOIQAgXrr4T1CCWUesncwwPBVCyy2Mw3Nmhmr9MrF8\nUlvdRKBxriRnlP3qJQIDAQAB\n-----END PUBLIC KEY-----", + "key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvryKBogPyv5P8jYRdZKb\n88Eg+gBul1FF5vkftzsVCFu8BvM5p64w5N4cAK6Rv/62MXRBi+sIhfIQ682C0rK9\nX8kbJLlu6QluvofB1r2qB5AIf8lOzJlzKlMgHN4qP4VcdB93QeyVmyL1dADPG5hI\npPgyP3gMW+a7QVr1BEfzqT26vNZm4e0n0v+9iG1W+Q9zjFIjQz1/+BM+F8yIMK74\n7Fpz+sMYLllLAJdElnTghT8E+3am6bVVDcHRKBpGeIz5f8ncVbWwHWwRqNjEVRUT\nBxXkLVQw4s4gvU+HgJHkhIhbwh+vEDpU1oY85ajO6NRuHqZbUPNc4rAccBSyJ+ze\nIQIDAQAB\n-----END PUBLIC KEY-----", "subject": "testuser@ourdomain.com" }, "Test Client ID PHP-5.2": { diff --git a/test/lib/OAuth2/Storage/BaseTest.php b/test/lib/OAuth2/Storage/BaseTest.php index e841d3ad2..9d236c012 100755 --- a/test/lib/OAuth2/Storage/BaseTest.php +++ b/test/lib/OAuth2/Storage/BaseTest.php @@ -6,13 +6,12 @@ abstract class BaseTest extends TestCase { - public function provideStorage() + public static function provideStorage() { $memory = Bootstrap::getInstance()->getMemoryStorage(); $sqlite = Bootstrap::getInstance()->getSqlitePdo(); $mysql = Bootstrap::getInstance()->getMysqlPdo(); $postgres = Bootstrap::getInstance()->getPostgresPdo(); - $mongo = Bootstrap::getInstance()->getMongo(); $mongoDb = Bootstrap::getInstance()->getMongoDB(); $redis = Bootstrap::getInstance()->getRedisStorage(); $cassandra = Bootstrap::getInstance()->getCassandraStorage(); @@ -27,7 +26,6 @@ public function provideStorage() array($sqlite), array($mysql), array($postgres), - array($mongo), array($mongoDb), array($redis), array($cassandra), diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 66c93ae5b..724b1dbef 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -4,13 +4,10 @@ class Bootstrap { - const DYNAMODB_PHP_VERSION = 'none'; - protected static $instance; private $mysql; private $sqlite; private $postgres; - private $mongo; private $mongoDb; private $redis; private $cassandra; @@ -68,7 +65,8 @@ public function getPostgresPdo() public function getPostgresDriver() { try { - $pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres', 'postgres'); + $pgHost = $this->getEnvVar('POSTGRES_HOST', 'localhost'); + $pdo = new \PDO("pgsql:host={$pgHost};dbname=oauth2_server_php", 'postgres', 'postgres'); return $pdo; } catch (\PDOException $e) { @@ -85,7 +83,8 @@ public function getRedisStorage() { if (!$this->redis) { if (class_exists('Predis\Client')) { - $redis = new \Predis\Client(); + $redisHost = $this->getEnvVar('REDIS_HOST', '127.0.0.1'); + $redis = new \Predis\Client(['host' => $redisHost]); if ($this->testRedisConnection($redis)) { $redis->flushdb(); $this->redis = new Redis($redis); @@ -118,9 +117,10 @@ public function getMysqlPdo() if (!$this->mysql) { $pdo = null; try { - $pdo = new \PDO('mysql:host=localhost;', 'root', 'root'); + $mysqlHost = $this->getEnvVar('MYSQL_HOST', '127.0.0.1'); + $pdo = new \PDO("mysql:host={$mysqlHost};", 'root', 'root'); } catch (\PDOException $e) { - $this->mysql = new NullStorage('MySQL', 'Unable to connect to MySQL on root@localhost'); + $this->mysql = new NullStorage('MySQL', "Unable to connect to MySQL on root@{$mysqlHost}"); } if ($pdo) { @@ -135,33 +135,12 @@ public function getMysqlPdo() return $this->mysql; } - public function getMongo() - { - if (!$this->mongo) { - if (class_exists('MongoClient')) { - $mongo = new \MongoClient('mongodb://localhost:27017', array('connect' => false)); - if ($this->testMongoConnection($mongo)) { - $db = $mongo->oauth2_server_php_legacy; - $this->removeMongo($db); - $this->createMongo($db); - - $this->mongo = new Mongo($db); - } else { - $this->mongo = new NullStorage('Mongo', 'Unable to connect to mongo server on "localhost:27017"'); - } - } else { - $this->mongo = new NullStorage('Mongo', 'Missing mongo php extension. Please install mongo.so'); - } - } - - return $this->mongo; - } - public function getMongoDb() { if (!$this->mongoDb) { - if (class_exists('MongoDB\Client')) { - $mongoDb = new \MongoDB\Client('mongodb://localhost:27017'); + if (extension_loaded('mongodb') && class_exists('MongoDB\Client')) { + $mongoHost = $this->getEnvVar('MONGODB_HOST', 'localhost'); + $mongoDb = new \MongoDB\Client("mongodb://{$mongoHost}:27017"); if ($this->testMongoDBConnection($mongoDb)) { $db = $mongoDb->oauth2_server_php; $this->removeMongoDb($db); @@ -179,17 +158,6 @@ public function getMongoDb() return $this->mongoDb; } - private function testMongoConnection(\MongoClient $mongo) - { - try { - $mongo->connect(); - } catch (\MongoConnectionException $e) { - return false; - } - - return true; - } - private function testMongoDBConnection(\MongoDB\Client $mongo) { return true; @@ -200,103 +168,73 @@ public function getCouchbase() if (!$this->couchbase) { if ($this->getEnvVar('SKIP_COUCHBASE_TESTS')) { $this->couchbase = new NullStorage('Couchbase', 'Skipping Couchbase tests'); - } elseif (!class_exists('Couchbase')) { - $this->couchbase = new NullStorage('Couchbase', 'Missing Couchbase php extension. Please install couchbase.so'); + } elseif (!extension_loaded('couchbase') || !class_exists(\Couchbase\ClusterOptions::class)) { + $this->couchbase = new NullStorage('Couchbase', 'Missing Couchbase SDK. Install ext-couchbase and couchbase/couchbase ^4.4'); } else { - // round-about way to make sure couchbase is working - // this is required because it throws a "floating point exception" otherwise - $code = "new \Couchbase(array('localhost:8091'), '', '', 'auth', false);"; - $exec = sprintf('php -r "%s"', $code); - $ret = exec($exec, $test, $var); - if ($ret != 0) { - $couchbase = new \Couchbase(array('localhost:8091'), '', '', 'auth', false); - if ($this->testCouchbaseConnection($couchbase)) { - $this->clearCouchbase($couchbase); - $this->createCouchbaseDB($couchbase); - - $this->couchbase = new CouchbaseDB($couchbase); - } else { - $this->couchbase = new NullStorage('Couchbase', 'Unable to connect to Couchbase server on "localhost:8091"'); - } - } else { - $this->couchbase = new NullStorage('Couchbase', 'Error while trying to connect to Couchbase'); - } - } - } + try { + $options = new \Couchbase\ClusterOptions(); + $options->credentials( + $this->getEnvVar('CB_USERNAME', 'Administrator'), + $this->getEnvVar('CB_PASSWORD', 'password') + ); + $cluster = new \Couchbase\Cluster( + $this->getEnvVar('CB_CONNECTION_STRING', 'couchbase://localhost'), + $options + ); + $bucket = $cluster->bucket($this->getEnvVar('CB_BUCKET', 'default')); + $collection = $bucket->defaultCollection(); - return $this->couchbase; - } + $this->clearCouchbase($collection); + $this->createCouchbaseDB($collection); - private function testCouchbaseConnection(\Couchbase $couchbase) - { - try { - if (count($couchbase->getServers()) > 0) { - return true; + $this->couchbase = new CouchbaseDB($collection); + } catch (\Exception $e) { + $this->couchbase = new NullStorage('Couchbase', 'Unable to connect to Couchbase: ' . $e->getMessage()); + } } - } catch (\CouchbaseException $e) { - return false; } - return true; + return $this->couchbase; } public function getCassandraStorage() { if (!$this->cassandra) { - if (class_exists('phpcassa\ColumnFamily')) { - $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160')); - if ($this->testCassandraConnection($cassandra)) { - $this->removeCassandraDb(); - $this->cassandra = new Cassandra($cassandra); - $this->createCassandraDb($this->cassandra); - } else { - $this->cassandra = new NullStorage('Cassandra', 'Unable to connect to cassandra server on "127.0.0.1:9160"'); - } - } else { - $this->cassandra = new NullStorage('Cassandra', 'Missing cassandra library. Please run "composer.phar require thobbs/phpcassa:dev-master"'); - } - } + if (!class_exists('Cassandra\Connection')) { + $this->cassandra = new NullStorage('Cassandra', 'Missing cassandra library. Please run "composer require mroosz/php-cassandra"'); - return $this->cassandra; - } + return $this->cassandra; + } - private function testCassandraConnection(\phpcassa\Connection\ConnectionPool $cassandra) - { - try { - new \phpcassa\SystemManager('localhost:9160'); - } catch (\Exception $e) { - return false; + try { + $cassandraHost = $this->getEnvVar('CASSANDRA_HOST', '127.0.0.1'); + $conn = new \Cassandra\Connection([ + new \Cassandra\Connection\StreamNodeConfig( + host: $cassandraHost, + port: 9042, + ), + ]); + $conn->connect(); + + // recreate keyspace + $conn->query("DROP KEYSPACE IF EXISTS oauth2_test"); + $conn->query("CREATE KEYSPACE oauth2_test WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1}"); + $conn->query("CREATE TABLE oauth2_test.oauth_data (key text PRIMARY KEY, value text)"); + + $conn->setKeyspace('oauth2_test'); + + $this->cassandra = new Cassandra($conn); + $this->createCassandraDb($this->cassandra, $conn); + } catch (\Exception $e) { + $this->cassandra = new NullStorage('Cassandra', $e->getMessage()); + } } - return true; - } - - private function removeCassandraDb() - { - $sys = new \phpcassa\SystemManager('localhost:9160'); - - try { - $sys->drop_keyspace('oauth2_test'); - } catch (\cassandra\InvalidRequestException $e) { - - } + return $this->cassandra; } - private function createCassandraDb(Cassandra $storage) + private function createCassandraDb(Cassandra $storage, \Cassandra\Connection $conn) { - // create the cassandra keyspace and column family - $sys = new \phpcassa\SystemManager('localhost:9160'); - - $sys->create_keyspace('oauth2_test', array( - "strategy_class" => \phpcassa\Schema\StrategyClass::SIMPLE_STRATEGY, - "strategy_options" => array('replication_factor' => '1') - )); - - $sys->create_column_family('oauth2_test', 'auth'); - $cassandra = new \phpcassa\Connection\ConnectionPool('oauth2_test', array('127.0.0.1:9160')); - $cf = new \phpcassa\ColumnFamily($cassandra, 'auth'); - - // populate the data $storage->setClientDetails("oauth_test_client", "testpass", "http://example.com", 'implicit password'); $storage->setAccessToken("testtoken", "Some Client", '', time() + 1000); $storage->setAuthorizationCode("testcode", "Some Client", '', '', time() + 1000); @@ -318,12 +256,24 @@ private function createCassandraDb(Cassandra $storage) $storage->setClientKey('oauth_test_client', $this->getTestPublicKey(), 'test_subject'); - $cf->insert("oauth_public_keys:ClientID_One", array('__data' => json_encode(array("public_key" => "client_1_public", "private_key" => "client_1_private", "encryption_algorithm" => "RS256")))); - $cf->insert("oauth_public_keys:ClientID_Two", array('__data' => json_encode(array("public_key" => "client_2_public", "private_key" => "client_2_private", "encryption_algorithm" => "RS256")))); - $cf->insert("oauth_public_keys:", array('__data' => json_encode(array("public_key" => $this->getTestPublicKey(), "private_key" => $this->getTestPrivateKey(), "encryption_algorithm" => "RS256")))); - - $cf->insert("oauth_users:testuser", array('__data' =>json_encode(array("password" => "password", "email" => "testuser@test.com", "email_verified" => true)))); - + // insert public keys and user directly + $table = 'oauth_data'; + $conn->query("INSERT INTO $table (key, value) VALUES (?, ?)", [ + 'oauth_public_keys:ClientID_One', + json_encode(array("public_key" => "client_1_public", "private_key" => "client_1_private", "encryption_algorithm" => "RS256")), + ]); + $conn->query("INSERT INTO $table (key, value) VALUES (?, ?)", [ + 'oauth_public_keys:ClientID_Two', + json_encode(array("public_key" => "client_2_public", "private_key" => "client_2_private", "encryption_algorithm" => "RS256")), + ]); + $conn->query("INSERT INTO $table (key, value) VALUES (?, ?)", [ + 'oauth_public_keys:', + json_encode(array("public_key" => $this->getTestPublicKey(), "private_key" => $this->getTestPrivateKey(), "encryption_algorithm" => "RS256")), + ]); + $conn->query("INSERT INTO $table (key, value) VALUES (?, ?)", [ + 'oauth_users:testuser', + json_encode(array("password" => "password", "email" => "testuser@test.com", "email_verified" => true)), + ]); } private function createSqliteDb(\PDO $pdo) @@ -352,11 +302,17 @@ private function removeMysqlDb(\PDO $pdo) private function createPostgresDb() { - if (!`PGPASSWORD=postgres psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='postgres'" -h localhost -U postgres`) { - `PGPASSWORD=postgres createuser -s -r postgres -h localhost -U postgres`; + try { + $pgHost = $this->getEnvVar('POSTGRES_HOST', 'localhost'); + $pdo = new \PDO("pgsql:host={$pgHost}", 'postgres', 'postgres'); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $exists = $pdo->query("SELECT 1 FROM pg_database WHERE datname = 'oauth2_server_php'")->fetchColumn(); + if (!$exists) { + $pdo->exec('CREATE DATABASE oauth2_server_php'); + } + } catch (\PDOException $e) { + // connection failed — will be caught later in getPostgresPdo } - - `PGPASSWORD=postgres createdb -O postgres oauth2_server_php -h localhost -U postgres`; } private function populatePostgresDb(\PDO $pdo) @@ -366,8 +322,15 @@ private function populatePostgresDb(\PDO $pdo) private function removePostgresDb() { - if (trim(`PGPASSWORD=postgres psql -l -h localhost -U postgres | grep oauth2_server_php | wc -l`)) { - `PGPASSWORD=postgres dropdb oauth2_server_php -h localhost -U postgres`; + try { + $pgHost = $this->getEnvVar('POSTGRES_HOST', 'localhost'); + $pdo = new \PDO("pgsql:host={$pgHost}", 'postgres', 'postgres'); + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + // terminate existing connections before dropping + $pdo->exec("SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = 'oauth2_server_php' AND pid <> pg_backend_pid()"); + $pdo->exec('DROP DATABASE IF EXISTS oauth2_server_php'); + } catch (\PDOException $e) { + // connection failed — will be caught later } } @@ -428,90 +391,54 @@ public function getConfigDir() return $this->configDir; } - private function createCouchbaseDB(\Couchbase $db) + private function createCouchbaseDB(\Couchbase\Collection $collection) { - $db->set('oauth_clients-oauth_test_client',json_encode(array( - 'client_id' => "oauth_test_client", - 'client_secret' => "testpass", - 'redirect_uri' => "http://example.com", - 'grant_types' => 'implicit password' - ))); - - $db->set('oauth_access_tokens-testtoken',json_encode(array( - 'access_token' => "testtoken", - 'client_id' => "Some Client" - ))); - - $db->set('oauth_authorization_codes-testcode',json_encode(array( - 'access_token' => "testcode", - 'client_id' => "Some Client" - ))); - - $db->set('oauth_users-testuser',json_encode(array( - 'username' => 'testuser', - 'password' => 'password', - 'email' => 'testuser@test.com', - 'email_verified' => true, - ))); - - $db->set('oauth_jwt-oauth_test_client',json_encode(array( + $collection->upsert('oauth_clients-oauth_test_client', [ 'client_id' => 'oauth_test_client', - 'key' => $this->getTestPublicKey(), - 'subject' => 'test_subject', - ))); - } - - private function clearCouchbase(\Couchbase $cb) - { - $cb->delete('oauth_authorization_codes-new-openid-code'); - $cb->delete('oauth_access_tokens-newtoken'); - $cb->delete('oauth_authorization_codes-newcode'); - $cb->delete('oauth_refresh_tokens-refreshtoken'); - } - - private function createMongo(\MongoDB $db) - { - $db->oauth_clients->insert(array( - 'client_id' => "oauth_test_client", - 'client_secret' => "testpass", - 'redirect_uri' => "http://example.com", - 'grant_types' => 'implicit password' - )); - - $db->oauth_access_tokens->insert(array( - 'access_token' => "testtoken", - 'client_id' => "Some Client" - )); - - $db->oauth_authorization_codes->insert(array( - 'authorization_code' => "testcode", - 'client_id' => "Some Client" - )); - - $db->oauth_users->insert(array( + 'client_secret' => 'testpass', + 'redirect_uri' => 'http://example.com', + 'grant_types' => 'implicit password', + ]); + + $collection->upsert('oauth_access_tokens-testtoken', [ + 'access_token' => 'testtoken', + 'client_id' => 'Some Client', + ]); + + $collection->upsert('oauth_authorization_codes-testcode', [ + 'access_token' => 'testcode', + 'client_id' => 'Some Client', + ]); + + $collection->upsert('oauth_users-testuser', [ 'username' => 'testuser', 'password' => 'password', 'email' => 'testuser@test.com', 'email_verified' => true, - )); - - $db->oauth_keys->insert(array( - 'client_id' => null, - 'public_key' => $this->getTestPublicKey(), - 'private_key' => $this->getTestPrivateKey(), - 'encryption_algorithm' => 'RS256' - )); + ]); - $db->oauth_jwt->insert(array( + $collection->upsert('oauth_jwt-oauth_test_client', [ 'client_id' => 'oauth_test_client', 'key' => $this->getTestPublicKey(), - 'subject' => 'test_subject', - )); + 'subject' => 'test_subject', + ]); } - public function removeMongo(\MongoDB $db) + private function clearCouchbase(\Couchbase\Collection $collection) { - $db->drop(); + $keys = [ + 'oauth_authorization_codes-new-openid-code', + 'oauth_access_tokens-newtoken', + 'oauth_authorization_codes-newcode', + 'oauth_refresh_tokens-refreshtoken', + ]; + foreach ($keys as $key) { + try { + $collection->remove($key); + } catch (\Couchbase\Exception\DocumentNotFoundException) { + // ignore + } + } } private function createMongoDB(\MongoDB\Database $db) @@ -597,223 +524,167 @@ private function getTestPrivateKey() public function getDynamoDbStorage() { if (!$this->dynamodb) { - // only run once per travis build - if (true == $this->getEnvVar('TRAVIS')) { - if (self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) { - $this->dynamodb = new NullStorage('DynamoDb', 'Skipping for travis.ci - only run once per build'); - - return; - } - } - if (class_exists('\Aws\DynamoDb\DynamoDbClient')) { - if ($client = $this->getDynamoDbClient()) { - // travis runs a unique set of tables per build, to avoid conflict - $prefix = ''; - if ($build_id = $this->getEnvVar('TRAVIS_JOB_NUMBER')) { - $prefix = sprintf('build_%s_', $build_id); - } else { - if (!$this->deleteDynamoDb($client, $prefix, true)) { - return $this->dynamodb = new NullStorage('DynamoDb', 'Timed out while waiting for DynamoDB deletion (30 seconds)'); - } - } - $this->createDynamoDb($client, $prefix); - $this->populateDynamoDb($client, $prefix); - $config = array( - 'client_table' => $prefix.'oauth_clients', - 'access_token_table' => $prefix.'oauth_access_tokens', - 'refresh_token_table' => $prefix.'oauth_refresh_tokens', - 'code_table' => $prefix.'oauth_authorization_codes', - 'user_table' => $prefix.'oauth_users', - 'jwt_table' => $prefix.'oauth_jwt', - 'scope_table' => $prefix.'oauth_scopes', - 'public_key_table' => $prefix.'oauth_public_keys', - ); - $this->dynamodb = new DynamoDB($client, $config); - } elseif (!$this->dynamodb) { - $this->dynamodb = new NullStorage('DynamoDb', 'unable to connect to DynamoDB'); - } - } else { - $this->dynamodb = new NullStorage('DynamoDb', 'Missing DynamoDB library. Please run "composer.phar require aws/aws-sdk-php:dev-master'); + try { + $this->initDynamoDbStorage(); + } catch (\Exception $e) { + $this->dynamodb = new NullStorage('DynamoDb', $e->getMessage()); } } return $this->dynamodb; } - private function getDynamoDbClient() + private function initDynamoDbStorage() { - $config = array(); - // check for environment variables - if (($key = $this->getEnvVar('AWS_ACCESS_KEY_ID')) && ($secret = $this->getEnvVar('AWS_SECRET_KEY'))) { - $config['key'] = $key; - $config['secret'] = $secret; - } else { - // fall back on ~/.aws/credentials file - // @see http://docs.aws.amazon.com/aws-sdk-php/guide/latest/credentials.html#credential-profiles - if (!file_exists($this->getEnvVar('HOME') . '/.aws/credentials')) { - $this->dynamodb = new NullStorage('DynamoDb', 'No aws credentials file found, and no AWS_ACCESS_KEY_ID or AWS_SECRET_KEY environment variable set'); + if (!class_exists('\Aws\DynamoDb\DynamoDbClient')) { + $this->dynamodb = new NullStorage('DynamoDb', 'Missing DynamoDB library. Please run "composer require aws/aws-sdk-php:^3.0"'); - return; - } + return; + } + + $endpoint = $this->getEnvVar('DYNAMODB_ENDPOINT', 'http://localhost:8000'); + + try { + $client = new \Aws\DynamoDb\DynamoDbClient([ + 'region' => 'us-east-1', + 'version' => 'latest', + 'endpoint' => $endpoint, + 'credentials' => [ + 'key' => 'fake', + 'secret' => 'fake', + ], + ]); + + // verify DynamoDB Local is reachable + $client->listTables(); + } catch (\Exception $e) { + $this->dynamodb = new NullStorage('DynamoDb', 'Unable to connect to DynamoDB Local at ' . $endpoint . ': ' . $e->getMessage()); - // set profile in AWS_PROFILE environment variable, defaults to "default" - $config['profile'] = $this->getEnvVar('AWS_PROFILE', 'default'); + return; } - // set region in AWS_REGION environment variable, defaults to "us-east-1" - $config['region'] = $this->getEnvVar('AWS_REGION', \Aws\Common\Enum\Region::US_EAST_1); + $prefix = 'test_'; + $this->deleteDynamoDb($client, $prefix); + $this->createDynamoDb($client, $prefix); + $this->populateDynamoDb($client, $prefix); - return \Aws\DynamoDb\DynamoDbClient::factory($config); + $config = [ + 'client_table' => $prefix.'oauth_clients', + 'access_token_table' => $prefix.'oauth_access_tokens', + 'refresh_token_table' => $prefix.'oauth_refresh_tokens', + 'code_table' => $prefix.'oauth_authorization_codes', + 'user_table' => $prefix.'oauth_users', + 'jwt_table' => $prefix.'oauth_jwt', + 'scope_table' => $prefix.'oauth_scopes', + 'public_key_table' => $prefix.'oauth_public_keys', + ]; + $this->dynamodb = new DynamoDB($client, $config); } - private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null, $waitForDeletion = false) + private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null) { $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); - $nbTables = count($tablesList); - // Delete all table. - foreach ($tablesList as $key => $table) { + foreach ($tablesList as $table) { try { - $client->deleteTable(array('TableName' => $prefix.$table)); + $client->deleteTable(['TableName' => $prefix.$table]); + $client->waitUntil('TableNotExists', ['TableName' => $prefix.$table]); } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { - // Table does not exist : nothing to do - } - } - - // Wait for deleting - if ($waitForDeletion) { - $retries = 5; - $nbTableDeleted = 0; - while ($nbTableDeleted != $nbTables) { - $nbTableDeleted = 0; - foreach ($tablesList as $key => $table) { - try { - $result = $client->describeTable(array('TableName' => $prefix.$table)); - } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { - // Table does not exist : nothing to do - $nbTableDeleted++; - } - } - if ($nbTableDeleted != $nbTables) { - if ($retries < 0) { - // we are tired of waiting - return false; - } - sleep(5); - echo "Sleeping 5 seconds for DynamoDB ($retries more retries)...\n"; - $retries--; - } + // Table does not exist } } - - return true; } private function createDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null) { - $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); - $nbTables = count($tablesList); - $client->createTable(array( + $client->createTable([ 'TableName' => $prefix.'oauth_access_tokens', - 'AttributeDefinitions' => array( - array('AttributeName' => 'access_token','AttributeType' => 'S') - ), - 'KeySchema' => array(array('AttributeName' => 'access_token','KeyType' => 'HASH')), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - $client->createTable(array( + 'AttributeDefinitions' => [ + ['AttributeName' => 'access_token', 'AttributeType' => 'S'], + ], + 'KeySchema' => [['AttributeName' => 'access_token', 'KeyType' => 'HASH']], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + $client->createTable([ 'TableName' => $prefix.'oauth_authorization_codes', - 'AttributeDefinitions' => array( - array('AttributeName' => 'authorization_code','AttributeType' => 'S') - ), - 'KeySchema' => array(array('AttributeName' => 'authorization_code','KeyType' => 'HASH')), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - $client->createTable(array( + 'AttributeDefinitions' => [ + ['AttributeName' => 'authorization_code', 'AttributeType' => 'S'], + ], + 'KeySchema' => [['AttributeName' => 'authorization_code', 'KeyType' => 'HASH']], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + $client->createTable([ 'TableName' => $prefix.'oauth_clients', - 'AttributeDefinitions' => array( - array('AttributeName' => 'client_id','AttributeType' => 'S') - ), - 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - $client->createTable(array( + 'AttributeDefinitions' => [ + ['AttributeName' => 'client_id', 'AttributeType' => 'S'], + ], + 'KeySchema' => [['AttributeName' => 'client_id', 'KeyType' => 'HASH']], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + $client->createTable([ 'TableName' => $prefix.'oauth_jwt', - 'AttributeDefinitions' => array( - array('AttributeName' => 'client_id','AttributeType' => 'S'), - array('AttributeName' => 'subject','AttributeType' => 'S') - ), - 'KeySchema' => array( - array('AttributeName' => 'client_id','KeyType' => 'HASH'), - array('AttributeName' => 'subject','KeyType' => 'RANGE') - ), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - $client->createTable(array( + 'AttributeDefinitions' => [ + ['AttributeName' => 'client_id', 'AttributeType' => 'S'], + ['AttributeName' => 'subject', 'AttributeType' => 'S'], + ], + 'KeySchema' => [ + ['AttributeName' => 'client_id', 'KeyType' => 'HASH'], + ['AttributeName' => 'subject', 'KeyType' => 'RANGE'], + ], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + $client->createTable([ 'TableName' => $prefix.'oauth_public_keys', - 'AttributeDefinitions' => array( - array('AttributeName' => 'client_id','AttributeType' => 'S') - ), - 'KeySchema' => array(array('AttributeName' => 'client_id','KeyType' => 'HASH')), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - $client->createTable(array( + 'AttributeDefinitions' => [ + ['AttributeName' => 'client_id', 'AttributeType' => 'S'], + ], + 'KeySchema' => [['AttributeName' => 'client_id', 'KeyType' => 'HASH']], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + $client->createTable([ 'TableName' => $prefix.'oauth_refresh_tokens', - 'AttributeDefinitions' => array( - array('AttributeName' => 'refresh_token','AttributeType' => 'S') - ), - 'KeySchema' => array(array('AttributeName' => 'refresh_token','KeyType' => 'HASH')), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - $client->createTable(array( + 'AttributeDefinitions' => [ + ['AttributeName' => 'refresh_token', 'AttributeType' => 'S'], + ], + 'KeySchema' => [['AttributeName' => 'refresh_token', 'KeyType' => 'HASH']], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + $client->createTable([ 'TableName' => $prefix.'oauth_scopes', - 'AttributeDefinitions' => array( - array('AttributeName' => 'scope','AttributeType' => 'S'), - array('AttributeName' => 'is_default','AttributeType' => 'S') - ), - 'KeySchema' => array(array('AttributeName' => 'scope','KeyType' => 'HASH')), - 'GlobalSecondaryIndexes' => array( - array( + 'AttributeDefinitions' => [ + ['AttributeName' => 'scope', 'AttributeType' => 'S'], + ['AttributeName' => 'is_default', 'AttributeType' => 'S'], + ], + 'KeySchema' => [['AttributeName' => 'scope', 'KeyType' => 'HASH']], + 'GlobalSecondaryIndexes' => [ + [ 'IndexName' => 'is_default-index', - 'KeySchema' => array(array('AttributeName' => 'is_default', 'KeyType' => 'HASH')), - 'Projection' => array('ProjectionType' => 'ALL'), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - ), - ), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - $client->createTable(array( + 'KeySchema' => [['AttributeName' => 'is_default', 'KeyType' => 'HASH']], + 'Projection' => ['ProjectionType' => 'ALL'], + ], + ], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + $client->createTable([ 'TableName' => $prefix.'oauth_users', - 'AttributeDefinitions' => array(array('AttributeName' => 'username','AttributeType' => 'S')), - 'KeySchema' => array(array('AttributeName' => 'username','KeyType' => 'HASH')), - 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) - )); - - // Wait for creation - $nbTableCreated = 0; - while ($nbTableCreated != $nbTables) { - $nbTableCreated = 0; - foreach ($tablesList as $key => $table) { - try { - $result = $client->describeTable(array('TableName' => $prefix.$table)); - if ($result['Table']['TableStatus'] == 'ACTIVE') { - $nbTableCreated++; - } - } catch (\Aws\DynamoDb\Exception\DynamoDbException $e) { - // Table does not exist : nothing to do - $nbTableCreated++; - } - } - if ($nbTableCreated != $nbTables) { - sleep(1); - } + 'AttributeDefinitions' => [ + ['AttributeName' => 'username', 'AttributeType' => 'S'], + ], + 'KeySchema' => [['AttributeName' => 'username', 'KeyType' => 'HASH']], + 'BillingMode' => 'PAY_PER_REQUEST', + ]); + + // Wait for all tables to become active + $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); + foreach ($tablesList as $table) { + $client->waitUntil('TableExists', ['TableName' => $prefix.$table]); } }