diff --git a/.gitignore b/.gitignore index 245087a..1bcb3fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.phpunit.result.cache /clover.xml /coveralls-upload.json /docs/html/ diff --git a/.travis.yml b/.travis.yml index 05e0d40..9eb9fcf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -sudo: false - language: php cache: @@ -19,6 +17,7 @@ matrix: - php: 7.1 env: - DEPS=locked + - LEGACY_DEPS="phpunit/phpunit" - CS_CHECK=true - TEST_COVERAGE=true - php: 7.1 @@ -33,14 +32,24 @@ matrix: - php: 7.2 env: - DEPS=latest + - php: 7.3 + env: + - DEPS=lowest + - php: 7.3 + env: + - DEPS=locked + - php: 7.3 + env: + - DEPS=latest before_install: - if [[ $TEST_COVERAGE != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi install: - - travis_retry composer install $COMPOSER_ARGS + - travis_retry composer install $COMPOSER_ARGS --ignore-platform-reqs + - if [[ $LEGACY_DEPS != '' ]]; then travis_retry composer update $COMPOSER_ARGS --with-dependencies $LEGACY_DEPS ; fi - if [[ $DEPS == 'latest' ]]; then travis_retry composer update $COMPOSER_ARGS ; fi - - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update --prefer-lowest --prefer-stable $COMPOSER_ARGS ; fi + - if [[ $DEPS == 'lowest' ]]; then travis_retry composer update $COMPOSER_ARGS --prefer-lowest --prefer-stable ; fi - if [[ $TEST_COVERAGE == 'true' ]]; then travis_retry composer require --dev $COMPOSER_ARGS $COVERAGE_DEPS ; fi - stty cols 120 && composer show @@ -53,4 +62,3 @@ after_script: notifications: email: false - diff --git a/CHANGELOG.md b/CHANGELOG.md index a28c5e7..7d0ddb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,210 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 2.0.1 - TBD + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 2.0.0 - 2019-12-28 + +### Added + +- Nothing. + +### Changed + +- [#69](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/69) updates the minimum supported version of league/oauth-server to ^8.0. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.3.0 - 2019-12-28 + +### Added + +- [#62](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/62) adds the ability to configure and add event listeners for the underlying league/oauth2 implementation. See the [event listeners configuration documentation](https://docs.zendframework.com/zend-expressive-authentication-oauth2/intro/#configure-event-listeners) for more information. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.2.1 - 2019-12-28 + +### Added + +- Nothing. + +### Changed + +- [#55](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/55) changes how the `OAuth2Adapter` validates when a client ID is present. Previously, if a client ID was present, but not a user ID, it would attempt to pull a user from the user factory using the client ID, which was incorrect. With this release, it no longer does that. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#71](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/71) adds a check to `AccessTokenRepository` to verify that a row was returned before checking if a token was revoked, raising an exception if not. + +- [#72](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/72) updates the database schema in provided examples to reflect actual requirements. + +## 1.2.0 - 2019-09-01 + +### Added + +- [#63](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/63) + registers the `ConfigProvider` with the package. If you are using zend-component-installer + it will be added to your configuration during the installation. + +- [#64](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/64) + adds support for PHP 7.3. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.1.0 - 2018-11-19 + +### Added + +- Nothing. + +### Changed + +- [#58](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/58) + Upgrades the `league/oauth2-server` library to 7.3.0 in order to use it with + [Swoole](https://www.swoole.co.uk/). This is provided by `league/oauth2-server` + thanks to [#960 AuthorizationServer stateless](https://github.com/thephpleague/oauth2-server/pull/960) + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.0.1 - 2018-10-31 + +### Added + +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- [#52](https://github.com/zendframework/zend-expressive-authentication-oauth2/issues/52) + Wrong factory mapped to AuthorizationHandler +- [#54](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/54) + Fixed "WWW-Authenticate" header value format + +## 1.0.0 - 2018-10-04 + +### Added + +- [#41](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/41) Allows + existing PDO service to be used. This will allow us to reuse existing pdo + services instead of opening up a second connection for oauth. +- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Adds `TokenEndpointHandler`, + `AuthorizationMiddleware` and `AuthorizationHandler` in the `Zend\Expressive\Authentication\OAuth2` namespace + to [implement an authorization server](https://docs.zendframework.com/zend-expressive-authentication-oauth2/v1/authorization-server/). +- [#50](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/50) Adds + all the OAuth2 identity data generated by [thephpleague/oauth2-server](https://github.com/thephpleague/oauth2-server) + to `UserInterface` PSR-7 attribute. These values are `oauth_user_id`, + `oauth_client_id`, `oauth_access_token_id`, `oauth_scopes`. + +### Changed + +- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Splits + `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware` into individual implementations that allow + [OAuth RFC-6749](https://tools.ietf.org/html/rfc6749) compliant authorization server implementations. + +### Deprecated + +- Nothing. + +### Removed + +- [#42](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/42) Removes + `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware`. + +### Fixed + +- [#44](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/44/) Fixes + revocation of access token for PDO repository +- [#45](https://github.com/zendframework/zend-expressive-authentication-oauth2/pull/45) Fixes + issue with empty scope being passed throwing exception. + ## 0.4.3 - 2018-05-09 ### Added diff --git a/LICENSE.md b/LICENSE.md index 4d6af70..ed2bafe 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) 2017-2018, Zend Technologies USA, Inc. +Copyright (c) 2017-2019, Zend Technologies USA, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, diff --git a/README.md b/README.md index 6c358a6..aaa8e09 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # OAuth2 server middleware for Expressive and PSR-7 applications +> ## Repository abandoned 2019-12-31 +> +> This repository has moved to [mezzio/mezzio-authentication-oauth2](https://github.com/mezzio/mezzio-authentication-oauth2). + [![Build Status](https://secure.travis-ci.org/zendframework/zend-expressive-authentication-oauth2.svg?branch=master)](https://secure.travis-ci.org/zendframework/zend-expressive-authentication-oauth2) [![Coverage Status](https://coveralls.io/repos/github/zendframework/zend-expressive-authentication-oauth2/badge.svg?branch=master)](https://coveralls.io/github/zendframework/zend-expressive-authentication-oauth2?branch=master) @@ -17,16 +21,6 @@ types: - implicit; - refresh token; -## WORK IN PROGRESS - -This repository contains a **work in progress** project for building an -authentication module for Expressive and PSR-7 applications. -It also provides an OAuth2 adapter for [zend-expressive-authentication](https://github.com/zendframework/zend-expressive-authentication) -library. - - -**Please, don't use this code in a production environment!** - ## Installation You can install the *zend-expressive-authentication-oauth2* library with @@ -38,10 +32,10 @@ $ composer require zendframework/zend-expressive-authentication-oauth2 ## Documentation -Documentation is [in the doc tree](doc/book/), and can be compiled using [mkdocs](http://www.mkdocs.org): +Browse the documentation online at https://docs.zendframework.com/zend-expressive-authentication-oauth2/ -```bash -$ mkdocs build -``` +## Support -You may also [browse the documentation online](https://docs.zendframework.com/zend-expressive-authentication-oauth2/). +* [Issues](https://github.com/zendframework/zend-expressive-authentication-oauth2/issues/) +* [Chat](https://zendframework-slack.herokuapp.com/) +* [Forum](https://discourse.zendframework.com/) diff --git a/composer.json b/composer.json index cca3743..180b8d4 100644 --- a/composer.json +++ b/composer.json @@ -17,22 +17,23 @@ "issues": "https://github.com/zendframework/zend-expressive-authentication-oauth2/issues", "source": "https://github.com/zendframework/zend-expressive-authentication-oauth2", "rss": "https://github.com/zendframework/zend-expressive-authentication-oauth2/releases.atom", - "slack": "https://zendframework-slack.herokuapp.com", + "chat": "https://zendframework-slack.herokuapp.com", "forum": "https://discourse.zendframework.com/c/questions/expressive" }, "require": { "php": "^7.1", - "league/oauth2-server": "^6.0.2", + "league/oauth2-server": "^8.0.0", "psr/container": "^1.0", "psr/http-message": "^1.0.1", "psr/http-server-middleware": "^1.0", - "zendframework/zend-expressive-authentication": "^0.4.0" + "zendframework/zend-expressive-authentication": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^7.0.1", + "phpunit/phpunit": "^7.5.15 || ^8.3.4", "roave/security-advisories": "dev-master", "zendframework/zend-coding-standard": "~1.0.0", - "zendframework/zend-diactoros": "^1.4.0" + "zendframework/zend-diactoros": "^1.4.0 || ^2.0", + "zendframework/zend-servicemanager": "^3.0.0" }, "conflict": { "container-interop/container-interop": "<1.2.0" @@ -52,7 +53,11 @@ }, "extra": { "branch-alias": { - "dev-master": "0.4.x-dev" + "dev-master": "2.0.x-dev", + "dev-develop": "2.1.x-dev" + }, + "zf": { + "config-provider": "Zend\\Expressive\\Authentication\\OAuth2\\ConfigProvider" } }, "bin": [ diff --git a/composer.lock b/composer.lock index a4d4585..1915661 100644 --- a/composer.lock +++ b/composer.lock @@ -1,32 +1,32 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "714de628f77f24ce564890f1614c06a3", + "content-hash": "607e2fc1e526e325c6d9b176d6250da4", "packages": [ { "name": "defuse/php-encryption", - "version": "v2.1.0", + "version": "v2.2.1", "source": { "type": "git", "url": "https://github.com/defuse/php-encryption.git", - "reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689" + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/defuse/php-encryption/zipball/5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689", - "reference": "5176f5abb38d3ea8a6e3ac6cd3bbb54d8185a689", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/0f407c43b953d571421e0020ba92082ed5fb7620", + "reference": "0f407c43b953d571421e0020ba92082ed5fb7620", "shasum": "" }, "require": { "ext-openssl": "*", - "paragonie/random_compat": "~2.0", + "paragonie/random_compat": ">= 2", "php": ">=5.4.0" }, "require-dev": { - "nikic/php-parser": "^2.0|^3.0", + "nikic/php-parser": "^2.0|^3.0|^4.0", "phpunit/phpunit": "^4|^5" }, "bin": [ @@ -67,37 +67,34 @@ "security", "symmetric key cryptography" ], - "time": "2017-05-18T21:28:48+00:00" + "time": "2018-07-24T23:27:56+00:00" }, { "name": "lcobucci/jwt", - "version": "3.2.2", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/lcobucci/jwt.git", - "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c" + "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0b5930be73582369e10c4d4bb7a12bac927a203c", - "reference": "0b5930be73582369e10c4d4bb7a12bac927a203c", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", + "reference": "a11ec5f4b4d75d1fcd04e133dede4c317aac9e18", "shasum": "" }, "require": { + "ext-mbstring": "*", "ext-openssl": "*", - "php": ">=5.5" + "php": "^5.6 || ^7.0" }, "require-dev": { - "mdanter/ecc": "~0.3.1", "mikey179/vfsstream": "~1.5", "phpmd/phpmd": "~2.2", "phpunit/php-invoker": "~1.1", - "phpunit/phpunit": "~4.5", + "phpunit/phpunit": "^5.7 || ^7.3", "squizlabs/php_codesniffer": "~2.3" }, - "suggest": { - "mdanter/ecc": "Required to use Elliptic Curves based algorithms." - }, "type": "library", "extra": { "branch-alias": { @@ -125,20 +122,20 @@ "JWS", "jwt" ], - "time": "2017-09-01T08:23:26+00:00" + "time": "2019-05-24T18:30:49+00:00" }, { "name": "league/event", - "version": "2.1.2", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/thephpleague/event.git", - "reference": "e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd" + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/event/zipball/e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd", - "reference": "e4bfc88dbcb60c8d8a2939a71f9813e141bbe4cd", + "url": "https://api.github.com/repos/thephpleague/event/zipball/d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119", "shasum": "" }, "require": { @@ -146,7 +143,7 @@ }, "require-dev": { "henrikbjorn/phpspec-code-coverage": "~1.0.1", - "phpspec/phpspec": "~2.0.0" + "phpspec/phpspec": "^2.2" }, "type": "library", "extra": { @@ -175,38 +172,41 @@ "event", "listener" ], - "time": "2015-05-21T12:24:47+00:00" + "time": "2018-11-26T11:52:41+00:00" }, { "name": "league/oauth2-server", - "version": "6.1.1", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/thephpleague/oauth2-server.git", - "reference": "a0cabb573c7cd5ee01803daec992d6ee3677c4ae" + "reference": "e1dc4d708c56fcfa205be4bb1862b6d525b4baac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/a0cabb573c7cd5ee01803daec992d6ee3677c4ae", - "reference": "a0cabb573c7cd5ee01803daec992d6ee3677c4ae", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/e1dc4d708c56fcfa205be4bb1862b6d525b4baac", + "reference": "e1dc4d708c56fcfa205be4bb1862b6d525b4baac", "shasum": "" }, "require": { - "defuse/php-encryption": "^2.1", + "defuse/php-encryption": "^2.2.1", + "ext-json": "*", "ext-openssl": "*", - "lcobucci/jwt": "^3.1", - "league/event": "^2.1", - "paragonie/random_compat": "^2.0", - "php": ">=5.6.0", - "psr/http-message": "^1.0" + "lcobucci/jwt": "^3.3.1", + "league/event": "^2.2", + "php": ">=7.1.0", + "psr/http-message": "^1.0.1" }, "replace": { "league/oauth2server": "*", "lncd/oauth2": "*" }, "require-dev": { - "phpunit/phpunit": "^4.8.38 || ^5.7.21", - "zendframework/zend-diactoros": "^1.0" + "phpstan/phpstan": "^0.11.8", + "phpstan/phpstan-phpunit": "^0.11.2", + "phpunit/phpunit": "^7.5.13 || ^8.2.3", + "roave/security-advisories": "dev-master", + "zendframework/zend-diactoros": "^2.1.2" }, "type": "library", "autoload": { @@ -224,6 +224,12 @@ "email": "hello@alexbilbie.com", "homepage": "http://www.alexbilbie.com", "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" } ], "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", @@ -243,37 +249,33 @@ "secure", "server" ], - "time": "2017-12-23T23:33:42+00:00" + "time": "2019-07-13T18:58:26+00:00" }, { "name": "paragonie/random_compat", - "version": "v2.0.11", + "version": "v9.99.99", "source": { "type": "git", "url": "https://github.com/paragonie/random_compat.git", - "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", - "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", + "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", "shasum": "" }, "require": { - "php": ">=5.2.0" + "php": "^7" }, "require-dev": { - "phpunit/phpunit": "4.*|5.*" + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" }, "suggest": { "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." }, "type": "library", - "autoload": { - "files": [ - "lib/random.php" - ] - }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" @@ -288,10 +290,11 @@ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", "keywords": [ "csprng", + "polyfill", "pseudorandom", "random" ], - "time": "2017-09-27T21:40:39+00:00" + "time": "2018-07-02T15:55:56+00:00" }, { "name": "psr/container", @@ -394,16 +397,16 @@ }, { "name": "psr/http-server-handler", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-server-handler.git", - "reference": "439d92054dc06097f2406ec074a2627839955a02" + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/439d92054dc06097f2406ec074a2627839955a02", - "reference": "439d92054dc06097f2406ec074a2627839955a02", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/aff2f80e33b7f026ec96bb42f63242dc50ffcae7", + "reference": "aff2f80e33b7f026ec96bb42f63242dc50ffcae7", "shasum": "" }, "require": { @@ -443,20 +446,20 @@ "response", "server" ], - "time": "2018-01-22T17:04:15+00:00" + "time": "2018-10-30T16:46:14+00:00" }, { "name": "psr/http-server-middleware", - "version": "1.0.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/php-fig/http-server-middleware.git", - "reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5" + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/ea17eb1fb2c8df6db919cc578451a8013c6a0ae5", - "reference": "ea17eb1fb2c8df6db919cc578451a8013c6a0ae5", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/2296f45510945530b9dceb8bcedb5cb84d40c5f5", + "reference": "2296f45510945530b9dceb8bcedb5cb84d40c5f5", "shasum": "" }, "require": { @@ -496,20 +499,20 @@ "request", "response" ], - "time": "2018-01-22T17:08:31+00:00" + "time": "2018-10-30T17:12:04+00:00" }, { "name": "zendframework/zend-expressive-authentication", - "version": "0.4.0", + "version": "1.1.0", "source": { "type": "git", "url": "https://github.com/zendframework/zend-expressive-authentication.git", - "reference": "2fea5529d78cddc26921e46e87ea886ccb8cc7d1" + "reference": "d602ab47649cd550ffc9c745cb6de862d6c8a862" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-expressive-authentication/zipball/2fea5529d78cddc26921e46e87ea886ccb8cc7d1", - "reference": "2fea5529d78cddc26921e46e87ea886ccb8cc7d1", + "url": "https://api.github.com/repos/zendframework/zend-expressive-authentication/zipball/d602ab47649cd550ffc9c745cb6de862d6c8a862", + "reference": "d602ab47649cd550ffc9c745cb6de862d6c8a862", "shasum": "" }, "require": { @@ -535,7 +538,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "0.4.x-dev" + "dev-master": "1.1.x-dev", + "dev-develop": "1.2.x-dev" }, "zf": { "config-provider": "Zend\\Expressive\\Authentication\\ConfigProvider" @@ -561,33 +565,67 @@ "zend-expressive", "zf" ], - "time": "2018-03-15T17:19:40+00:00" + "time": "2019-03-05T17:50:29+00:00" } ], "packages-dev": [ + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "abandoned": "psr/container", + "time": "2017-02-14T19:40:03+00:00" + }, { "name": "doctrine/instantiator", - "version": "1.1.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", - "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/ae466f726242e637cebdd526a7d991b9433bacf1", + "reference": "ae466f726242e637cebdd526a7d991b9433bacf1", "shasum": "" }, "require": { "php": "^7.1" }, "require-dev": { - "athletic/athletic": "~0.1.8", + "doctrine/coding-standard": "^6.0", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "^6.2.3", - "squizlabs/php_codesniffer": "^3.0.2" + "phpbench/phpbench": "^0.13", + "phpstan/phpstan-phpunit": "^0.11", + "phpstan/phpstan-shim": "^0.11", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { @@ -612,34 +650,37 @@ } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://github.com/doctrine/instantiator", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", "keywords": [ "constructor", "instantiate" ], - "time": "2017-07-22T11:58:36+00:00" + "time": "2019-10-21T16:45:58+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.7.0", + "version": "1.9.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" + "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", - "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/579bb7356d91f9456ccd505f24ca8b667966a0a7", + "reference": "579bb7356d91f9456ccd505f24ca8b667966a0a7", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.1" + }, + "replace": { + "myclabs/deep-copy": "self.version" }, "require-dev": { "doctrine/collections": "^1.0", "doctrine/common": "^2.6", - "phpunit/phpunit": "^4.1" + "phpunit/phpunit": "^7.1" }, "type": "library", "autoload": { @@ -662,26 +703,26 @@ "object", "object graph" ], - "time": "2017-10-19T19:58:43+00:00" + "time": "2019-12-15T19:12:40+00:00" }, { "name": "phar-io/manifest", - "version": "1.0.1", + "version": "1.0.3", "source": { "type": "git", "url": "https://github.com/phar-io/manifest.git", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", - "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", + "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4", "shasum": "" }, "require": { "ext-dom": "*", "ext-phar": "*", - "phar-io/version": "^1.0.1", + "phar-io/version": "^2.0", "php": "^5.6 || ^7.0" }, "type": "library", @@ -717,20 +758,20 @@ } ], "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", - "time": "2017-03-05T18:14:27+00:00" + "time": "2018-07-08T19:23:20+00:00" }, { "name": "phar-io/version", - "version": "1.0.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", - "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6", + "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6", "shasum": "" }, "require": { @@ -764,39 +805,37 @@ } ], "description": "Library for handling version information and constraints", - "time": "2017-03-05T17:38:23+00:00" + "time": "2018-07-08T19:19:57+00:00" }, { "name": "phpdocumentor/reflection-common", - "version": "1.0.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", - "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/63a995caa1ca9e5590304cd845c15ad6d482a62a", + "reference": "63a995caa1ca9e5590304cd845c15ad6d482a62a", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^4.6" + "phpunit/phpunit": "~6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src" - ] + "phpDocumentor\\Reflection\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -818,31 +857,32 @@ "reflection", "static analysis" ], - "time": "2017-09-11T18:02:19+00:00" + "time": "2018-08-07T13:53:10+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "4.3.0", + "version": "4.3.4", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08" + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94fd0001232e47129dd3504189fa1c7225010d08", - "reference": "94fd0001232e47129dd3504189fa1c7225010d08", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c", + "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c", "shasum": "" }, "require": { "php": "^7.0", - "phpdocumentor/reflection-common": "^1.0.0", - "phpdocumentor/type-resolver": "^0.4.0", + "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0", + "phpdocumentor/type-resolver": "~0.4 || ^1.0.0", "webmozart/assert": "^1.0" }, "require-dev": { - "doctrine/instantiator": "~1.0.5", + "doctrine/instantiator": "^1.0.5", "mockery/mockery": "^1.0", + "phpdocumentor/type-resolver": "0.4.*", "phpunit/phpunit": "^6.4" }, "type": "library", @@ -869,41 +909,40 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-11-30T07:14:17+00:00" + "time": "2019-12-28T18:55:12+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.4.0", + "version": "1.0.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", - "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", + "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9", "shasum": "" }, "require": { - "php": "^5.5 || ^7.0", - "phpdocumentor/reflection-common": "^1.0" + "php": "^7.1", + "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^5.2||^4.8.24" + "ext-tokenizer": "^7.1", + "mockery/mockery": "~1", + "phpunit/phpunit": "^7.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.x-dev" } }, "autoload": { "psr-4": { - "phpDocumentor\\Reflection\\": [ - "src/" - ] + "phpDocumentor\\Reflection\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -916,42 +955,43 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-07-14T14:27:02+00:00" + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "time": "2019-08-22T18:11:29+00:00" }, { "name": "phpspec/prophecy", - "version": "1.7.5", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401" + "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/dfd6be44111a7c41c2e884a336cc4f461b3b2401", - "reference": "dfd6be44111a7c41c2e884a336cc4f461b3b2401", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc", + "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", - "sebastian/comparator": "^1.1|^2.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", + "sebastian/comparator": "^1.2.3|^2.0|^3.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { - "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5" + "phpspec/phpspec": "^2.5 || ^3.2", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev" + "dev-master": "1.10.x-dev" } }, "autoload": { - "psr-0": { - "Prophecy\\": "src/" + "psr-4": { + "Prophecy\\": "src/Prophecy" } }, "notification-url": "https://packagist.org/downloads/", @@ -979,44 +1019,44 @@ "spy", "stub" ], - "time": "2018-02-19T10:16:54+00:00" + "time": "2019-12-22T21:05:45+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "6.0.1", + "version": "7.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba" + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f8ca4b604baf23dab89d87773c28cc07405189ba", - "reference": "f8ca4b604baf23dab89d87773c28cc07405189ba", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf", + "reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^7.1", - "phpunit/php-file-iterator": "^1.4.2", + "php": "^7.2", + "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.0", + "phpunit/php-token-stream": "^3.1.1", "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^3.0", + "sebastian/environment": "^4.2.2", "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" + "theseer/tokenizer": "^1.1.3" }, "require-dev": { - "phpunit/phpunit": "^7.0" + "phpunit/phpunit": "^8.2.2" }, "suggest": { - "ext-xdebug": "^2.6.0" + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.0-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -1042,29 +1082,32 @@ "testing", "xunit" ], - "time": "2018-02-02T07:01:41+00:00" + "time": "2019-11-20T13:55:58+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.5", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" + "reference": "050bedf145a257b1ff02746c31894800e5122946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", - "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/050bedf145a257b1ff02746c31894800e5122946", + "reference": "050bedf145a257b1ff02746c31894800e5122946", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -1079,7 +1122,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1089,7 +1132,7 @@ "filesystem", "iterator" ], - "time": "2017-11-27T13:52:08+00:00" + "time": "2018-09-13T20:33:42+00:00" }, { "name": "phpunit/php-text-template", @@ -1134,16 +1177,16 @@ }, { "name": "phpunit/php-timer", - "version": "2.0.0", + "version": "2.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f" + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/8b8454ea6958c3dee38453d3bd571e023108c91f", - "reference": "8b8454ea6958c3dee38453d3bd571e023108c91f", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/1038454804406b0b5f5f520358e78c1c2f71501e", + "reference": "1038454804406b0b5f5f520358e78c1c2f71501e", "shasum": "" }, "require": { @@ -1155,7 +1198,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -1179,20 +1222,20 @@ "keywords": [ "timer" ], - "time": "2018-02-01T13:07:23+00:00" + "time": "2019-06-07T04:22:29+00:00" }, { "name": "phpunit/php-token-stream", - "version": "3.0.0", + "version": "3.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace" + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/21ad88bbba7c3d93530d93994e0a33cd45f02ace", - "reference": "21ad88bbba7c3d93530d93994e0a33cd45f02ace", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/995192df77f63a59e47f025390d2d1fdf8f425ff", + "reference": "995192df77f63a59e47f025390d2d1fdf8f425ff", "shasum": "" }, "require": { @@ -1205,7 +1248,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } }, "autoload": { @@ -1228,53 +1271,56 @@ "keywords": [ "tokenizer" ], - "time": "2018-02-01T13:16:43+00:00" + "time": "2019-09-17T06:23:10+00:00" }, { "name": "phpunit/phpunit", - "version": "7.0.2", + "version": "8.5.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9" + "reference": "7870c78da3c5e4883eaef36ae47853ebb3cb86f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9", - "reference": "e2f8aa21bc54b6ba218bdd4f9e0dac1e9bc3b4e9", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/7870c78da3c5e4883eaef36ae47853ebb3cb86f2", + "reference": "7870c78da3c5e4883eaef36ae47853ebb3cb86f2", "shasum": "" }, "require": { + "doctrine/instantiator": "^1.2.0", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "^1.6.1", - "phar-io/manifest": "^1.0.1", - "phar-io/version": "^1.0", - "php": "^7.1", - "phpspec/prophecy": "^1.7", - "phpunit/php-code-coverage": "^6.0", - "phpunit/php-file-iterator": "^1.4.3", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.9.1", + "phar-io/manifest": "^1.0.3", + "phar-io/version": "^2.0.1", + "php": "^7.2", + "phpspec/prophecy": "^1.8.1", + "phpunit/php-code-coverage": "^7.0.7", + "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-timer": "^2.0", - "phpunit/phpunit-mock-objects": "^6.0", - "sebastian/comparator": "^2.1", - "sebastian/diff": "^3.0", - "sebastian/environment": "^3.1", - "sebastian/exporter": "^3.1", - "sebastian/global-state": "^2.0", + "phpunit/php-timer": "^2.1.2", + "sebastian/comparator": "^3.0.2", + "sebastian/diff": "^3.0.2", + "sebastian/environment": "^4.2.2", + "sebastian/exporter": "^3.1.1", + "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", - "sebastian/resource-operations": "^1.0", + "sebastian/resource-operations": "^2.0.1", + "sebastian/type": "^1.1.3", "sebastian/version": "^2.0.1" }, "require-dev": { "ext-pdo": "*" }, "suggest": { + "ext-soap": "*", "ext-xdebug": "*", - "phpunit/php-invoker": "^2.0" + "phpunit/php-invoker": "^2.0.0" }, "bin": [ "phpunit" @@ -1282,7 +1328,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "7.0-dev" + "dev-master": "8.5-dev" } }, "autoload": { @@ -1308,63 +1354,59 @@ "testing", "xunit" ], - "time": "2018-02-26T07:03:12+00:00" + "time": "2019-12-25T14:49:39+00:00" }, { - "name": "phpunit/phpunit-mock-objects", - "version": "6.0.1", + "name": "psr/http-factory", + "version": "1.0.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53" + "url": "https://github.com/php-fig/http-factory.git", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/e3249dedc2d99259ccae6affbc2684eac37c2e53", - "reference": "e3249dedc2d99259ccae6affbc2684eac37c2e53", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be", + "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.5", - "php": "^7.1", - "phpunit/php-text-template": "^1.2.1", - "sebastian/exporter": "^3.1" - }, - "require-dev": { - "phpunit/phpunit": "^7.0" - }, - "suggest": { - "ext-soap": "*" + "php": ">=7.0.0", + "psr/http-message": "^1.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "6.0.x-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { - "classmap": [ - "src/" - ] + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], "authors": [ { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" } ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "description": "Common interfaces for PSR-7 HTTP message factories", "keywords": [ - "mock", - "xunit" + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" ], - "time": "2018-02-15T05:27:38+00:00" + "time": "2019-04-30T12:38:16+00:00" }, { "name": "roave/security-advisories", @@ -1372,31 +1414,36 @@ "source": { "type": "git", "url": "https://github.com/Roave/SecurityAdvisories.git", - "reference": "664836e89c7ecad3dbaabc1572ea752c0d532d80" + "reference": "44a677c8e06241a66409ae6e4820dc166fc09ab2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/664836e89c7ecad3dbaabc1572ea752c0d532d80", - "reference": "664836e89c7ecad3dbaabc1572ea752c0d532d80", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/44a677c8e06241a66409ae6e4820dc166fc09ab2", + "reference": "44a677c8e06241a66409ae6e4820dc166fc09ab2", "shasum": "" }, "conflict": { "3f/pygmentize": "<1.2", - "adodb/adodb-php": "<5.20.6", + "adodb/adodb-php": "<5.20.12", + "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", "amphp/artax": "<1.0.6|>=2,<2.0.6", + "amphp/http": "<1.0.1", + "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6", "asymmetricrypt/asymmetricrypt": ">=0,<9.9.99", "aws/aws-sdk-php": ">=3,<3.2.1", + "brightlocal/phpwhois": "<=4.2.5", "bugsnag/bugsnag-laravel": ">=2,<2.0.2", - "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.0.15|>=3.1,<3.1.4", + "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.5.18|>=3.6,<3.6.15|>=3.7,<3.7.7", "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", "cartalyst/sentry": "<=2.1.6", "codeigniter/framework": "<=3.0.6", - "composer/composer": "<=1.0.0-alpha11", + "composer/composer": "<=1-alpha.11", "contao-components/mediaelement": ">=2.14.2,<2.21.1", - "contao/core": ">=2,<3.5.32", - "contao/core-bundle": ">=4,<4.4.8", + "contao/core": ">=2,<3.5.39", + "contao/core-bundle": ">=4,<4.4.46|>=4.5,<4.8.6", "contao/listing-bundle": ">=4,<4.4.8", - "contao/newsletter-bundle": ">=4,<4.1", + "datadog/dd-trace": ">=0.30,<0.30.2", + "david-garcia/phpwhois": "<=4.3.1", "doctrine/annotations": ">=1,<1.2.7", "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", "doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1", @@ -1407,89 +1454,147 @@ "doctrine/mongodb-odm-bundle": ">=2,<3.0.1", "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1", "dompdf/dompdf": ">=0.6,<0.6.2", - "drupal/core": ">=8,<8.4.5", - "drupal/drupal": ">=8,<8.4.5", - "erusev/parsedown": "<1.7", - "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.3|>=5.4,<5.4.11.3|>=2017.8,<2017.8.1.1|>=2017.12,<2017.12.2.1", + "drupal/core": ">=7,<8.7.11|>=8.8,<8.8.1", + "drupal/drupal": ">=7,<8.7.11|>=8.8,<8.8.1", + "erusev/parsedown": "<1.7.2", + "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.4", + "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.13.1|>=6,<6.7.9.1|>=6.8,<6.13.5.1|>=7,<7.2.4.1|>=7.3,<7.3.2.1", + "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.12.3|>=2011,<2017.12.4.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3", + "ezsystems/repository-forms": ">=2.3,<2.3.2.1", + "ezyang/htmlpurifier": "<4.1.1", "firebase/php-jwt": "<2", + "fooman/tcpdf": "<6.2.22", + "fossar/tcpdf-parser": "<6.2.22", "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", + "fuel/core": "<1.8.1", "gree/jose": "<=2.2", "gregwar/rst": "<1.0.3", - "guzzlehttp/guzzle": ">=6,<6.2.1|>=4.0.0-rc2,<4.2.4|>=5,<5.3.1", - "illuminate/auth": ">=4,<4.0.99|>=4.1,<4.1.26", + "guzzlehttp/guzzle": ">=4-rc.2,<4.2.4|>=5,<5.3.1|>=6,<6.2.1", + "illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29", + "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "ivankristianto/phpwhois": "<=4.3", + "james-heinrich/getid3": "<1.9.9", "joomla/session": "<1.3.1", - "laravel/framework": ">=4,<4.0.99|>=4.1,<4.1.29", + "jsmitty12/phpwhois": "<5.1", + "kazist/phpwhois": "<=4.2.6", + "kreait/firebase-php": ">=3.2,<3.8.1", + "la-haute-societe/tcpdf": "<6.2.22", + "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30", "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", - "magento/magento1ce": ">=1.5.0.1,<1.9.3.2", - "magento/magento1ee": ">=1.9,<1.14.3.2", - "magento/magento2ce": ">=2,<2.2", + "league/commonmark": "<0.18.3", + "magento/magento1ce": "<1.9.4.3", + "magento/magento1ee": ">=1,<1.14.4.3", + "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2", "monolog/monolog": ">=1.8,<1.12", "namshi/jose": "<2.2", "onelogin/php-saml": "<2.10.4", + "openid/php-openid": "<2.3", "oro/crm": ">=1.7,<1.7.4", "oro/platform": ">=1.7,<1.7.4", "padraic/humbug_get_contents": "<1.1.2", "pagarme/pagarme-php": ">=0,<3", "paragonie/random_compat": "<2", - "phpmailer/phpmailer": ">=5,<5.2.24", + "paypal/merchant-sdk-php": "<3.12", + "pear/archive_tar": "<1.4.4", + "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6", + "phpoffice/phpexcel": "<=1.8.1", + "phpoffice/phpspreadsheet": "<=1.5", "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpwhois/phpwhois": "<=4.2.5", "phpxmlrpc/extras": "<0.6.1", - "propel/propel": ">=2.0.0-alpha1,<=2.0.0-alpha7", + "propel/propel": ">=2-alpha.1,<=2-alpha.7", "propel/propel1": ">=1,<=1.7.1", "pusher/pusher-php-server": "<2.2.1", + "robrichards/xmlseclibs": ">=1,<3.0.4", "sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9", + "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11", + "sensiolabs/connect": "<4.2.3", + "serluck/phpwhois": "<=4.2.6", "shopware/shopware": "<5.3.7", "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11", "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", - "silverstripe/framework": ">=3,<3.3", + "silverstripe/framework": ">=3,<3.6.7|>=3.7,<3.7.3|>=4,<4.4", + "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.1.2", + "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", + "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4", "silverstripe/userforms": "<3", + "simple-updates/phpwhois": "<=1", "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4", - "simplesamlphp/simplesamlphp": "<1.15.2", + "simplesamlphp/simplesamlphp": "<1.17.8", "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", + "slim/slim": "<2.6", + "smarty/smarty": "<3.1.33", "socalnick/scn-social-auth": "<1.15.2", - "squizlabs/php_codesniffer": ">=1,<2.8.1", + "spoonity/tcpdf": "<6.2.22", + "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", "stormpath/sdk": ">=0,<9.9.99", + "studio-42/elfinder": "<2.1.48", "swiftmailer/swiftmailer": ">=4,<5.4.5", - "symfony/dependency-injection": ">=2,<2.0.17", - "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", - "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2", - "symfony/http-foundation": ">=2,<2.3.27|>=2.4,<2.5.11|>=2.6,<2.6.6", - "symfony/http-kernel": ">=2,<2.3.29|>=2.4,<2.5.12|>=2.6,<2.6.8", + "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", + "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", + "sylius/grid-bundle": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", + "sylius/sylius": ">=1,<1.1.18|>=1.2,<1.2.17|>=1.3,<1.3.12|>=1.4,<1.4.4", + "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", + "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", + "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/mime": ">=4.3,<4.3.8", + "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/polyfill": ">=1,<1.10", + "symfony/polyfill-php55": ">=1,<1.10", + "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", "symfony/routing": ">=2,<2.0.19", - "symfony/security": ">=2,<2.0.25|>=2.1,<2.1.13|>=2.2,<2.2.9|>=2.3,<2.3.37|>=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8.23,<2.8.25|>=3.2.10,<3.2.12|>=3.3.3,<3.3.5", - "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.6|>=2.8.23,<2.8.25|>=3,<3.0.6|>=3.2.10,<3.2.12|>=3.3.3,<3.3.5", - "symfony/security-csrf": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", - "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/security": ">=2,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7", + "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8", "symfony/serializer": ">=2,<2.0.11", - "symfony/symfony": ">=2,<2.3.41|>=2.4,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/symfony": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", "symfony/translation": ">=2,<2.0.17", "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", + "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7", + "tecnickcom/tcpdf": "<6.2.22", "thelia/backoffice-default-template": ">=2.1,<2.1.2", - "thelia/thelia": ">=2.1,<2.1.2|>=2.1.0-beta1,<2.1.3", + "thelia/thelia": ">=2.1-beta.1,<2.1.3", + "theonedemon/phpwhois": "<=4.2.5", "titon/framework": ">=0,<9.9.99", - "twig/twig": "<1.20", - "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.22|>=8,<8.7.5", + "truckersmp/phpwhois": "<=4.3.1", + "twig/twig": "<1.38|>=2,<2.7", + "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1", + "typo3/cms-core": ">=8,<8.7.30|>=9,<9.5.12|>=10,<10.2.1", "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5", "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4", + "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", + "ua-parser/uap-php": "<3.8", + "wallabag/tcpdf": "<6.2.22", "willdurand/js-translation-bundle": "<2.1.1", "yiisoft/yii": ">=1.1.14,<1.1.15", - "yiisoft/yii2": "<2.0.14", + "yiisoft/yii2": "<2.0.15", "yiisoft/yii2-bootstrap": "<2.0.4", - "yiisoft/yii2-dev": "<2.0.14", + "yiisoft/yii2-dev": "<2.0.15", + "yiisoft/yii2-elasticsearch": "<2.0.5", "yiisoft/yii2-gii": "<2.0.4", "yiisoft/yii2-jui": "<2.0.4", + "yiisoft/yii2-redis": "<2.0.8", "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", "zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5", - "zendframework/zend-diactoros": ">=1,<1.0.4", + "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3", + "zendframework/zend-diactoros": ">=1,<1.8.4", + "zendframework/zend-feed": ">=1,<2.10.3", "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1", - "zendframework/zend-http": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.3,<2.3.8|>=2.4,<2.4.1", + "zendframework/zend-http": ">=1,<2.8.1", "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", "zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2", @@ -1498,7 +1603,7 @@ "zendframework/zend-validator": ">=2.3,<2.3.6", "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", - "zendframework/zendframework": ">=2,<2.4.11|>=2.5,<2.5.1", + "zendframework/zendframework": "<2.5.1", "zendframework/zendframework1": "<1.12.20", "zendframework/zendopenid": ">=2,<2.0.2", "zendframework/zendxml": ">=1,<1.0.1", @@ -1520,7 +1625,7 @@ } ], "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", - "time": "2018-03-07T15:45:44+00:00" + "time": "2019-12-26T14:16:40+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", @@ -1569,30 +1674,30 @@ }, { "name": "sebastian/comparator", - "version": "2.1.3", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9" + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/34369daee48eafb2651bea869b4b15d75ccc35f9", - "reference": "34369daee48eafb2651bea869b4b15d75ccc35f9", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/5de4fc177adf9bce8df98d8d141a7559d7ccf6da", + "reference": "5de4fc177adf9bce8df98d8d141a7559d7ccf6da", "shasum": "" }, "require": { - "php": "^7.0", - "sebastian/diff": "^2.0 || ^3.0", + "php": "^7.1", + "sebastian/diff": "^3.0", "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^6.4" + "phpunit/phpunit": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.1.x-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1629,27 +1734,27 @@ "compare", "equality" ], - "time": "2018-02-01T13:46:46+00:00" + "time": "2018-07-12T15:12:46+00:00" }, { "name": "sebastian/diff", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8" + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/e09160918c66281713f1c324c1f4c4c3037ba1e8", - "reference": "e09160918c66281713f1c324c1f4c4c3037ba1e8", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/720fcc7e9b5cf384ea68d9d930d480907a0c1a29", + "reference": "720fcc7e9b5cf384ea68d9d930d480907a0c1a29", "shasum": "" }, "require": { "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^7.0", + "phpunit/phpunit": "^7.5 || ^8.0", "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", @@ -1685,32 +1790,35 @@ "unidiff", "unified diff" ], - "time": "2018-02-01T13:45:15+00:00" + "time": "2019-02-04T06:01:07+00:00" }, { "name": "sebastian/environment", - "version": "3.1.0", + "version": "4.2.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", - "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368", + "reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.1" }, "require-dev": { - "phpunit/phpunit": "^6.1" + "phpunit/phpunit": "^7.5" + }, + "suggest": { + "ext-posix": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1.x-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -1735,20 +1843,20 @@ "environment", "hhvm" ], - "time": "2017-07-01T08:51:00+00:00" + "time": "2019-11-20T08:46:58+00:00" }, { "name": "sebastian/exporter", - "version": "3.1.0", + "version": "3.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", - "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/68609e1261d215ea5b21b7987539cbfbe156ec3e", + "reference": "68609e1261d215ea5b21b7987539cbfbe156ec3e", "shasum": "" }, "require": { @@ -1775,6 +1883,10 @@ "BSD-3-Clause" ], "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, { "name": "Jeff Welch", "email": "whatthejeff@gmail.com" @@ -1783,17 +1895,13 @@ "name": "Volker Dusch", "email": "github@wallbash.com" }, - { - "name": "Bernhard Schussek", - "email": "bschussek@2bepublished.at" - }, - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" - }, { "name": "Adam Harvey", "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" } ], "description": "Provides the functionality to export PHP variables for visualization", @@ -1802,27 +1910,30 @@ "export", "exporter" ], - "time": "2017-04-03T13:19:02+00:00" + "time": "2019-09-14T09:02:43+00:00" }, { "name": "sebastian/global-state", - "version": "2.0.0", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", - "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", + "reference": "edf8a461cf1d4005f19fb0b6b8b95a9f7fa0adc4", "shasum": "" }, "require": { - "php": "^7.0" + "php": "^7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^6.0" + "ext-dom": "*", + "phpunit/phpunit": "^8.0" }, "suggest": { "ext-uopz": "*" @@ -1830,7 +1941,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -1853,7 +1964,7 @@ "keywords": [ "global state" ], - "time": "2017-04-27T15:39:26+00:00" + "time": "2019-02-01T05:30:01+00:00" }, { "name": "sebastian/object-enumerator", @@ -2002,25 +2113,25 @@ }, { "name": "sebastian/resource-operations", - "version": "1.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", - "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/4d7a795d35b889bf80a0cc04e08d77cedfa917a9", + "reference": "4d7a795d35b889bf80a0cc04e08d77cedfa917a9", "shasum": "" }, "require": { - "php": ">=5.6.0" + "php": "^7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2040,7 +2151,53 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2015-07-28T20:34:47+00:00" + "time": "2018-10-04T04:07:39+00:00" + }, + { + "name": "sebastian/type", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/3aaaa15fa71d27650d62a948be022fe3b48541a3", + "reference": "3aaaa15fa71d27650d62a948be022fe3b48541a3", + "shasum": "" + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "time": "2019-07-02T08:10:15+00:00" }, { "name": "sebastian/version", @@ -2087,16 +2244,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "2.9.1", + "version": "2.9.2", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "dcbed1074f8244661eecddfc2a675430d8d33f62" + "reference": "2acf168de78487db620ab4bc524135a13cfe6745" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dcbed1074f8244661eecddfc2a675430d8d33f62", - "reference": "dcbed1074f8244661eecddfc2a675430d8d33f62", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/2acf168de78487db620ab4bc524135a13cfe6745", + "reference": "2acf168de78487db620ab4bc524135a13cfe6745", "shasum": "" }, "require": { @@ -2161,20 +2318,78 @@ "phpcs", "standards" ], - "time": "2017-05-22T02:43:20+00:00" + "time": "2018-11-07T22:31:41+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.13.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.13-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-11-27T13:56:44+00:00" }, { "name": "theseer/tokenizer", - "version": "1.1.0", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", - "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", "shasum": "" }, "require": { @@ -2201,35 +2416,33 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2017-04-07T12:08:54+00:00" + "time": "2019-06-13T22:48:21+00:00" }, { "name": "webmozart/assert", - "version": "1.3.0", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a" + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a", - "reference": "0df1908962e7a3071564e857d86874dad1ef204a", + "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", + "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0" + "php": "^5.3.3 || ^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "vimeo/psalm": "<3.6.0" }, "require-dev": { - "phpunit/phpunit": "^4.6", - "sebastian/version": "^1.0.1" + "phpunit/phpunit": "^4.8.36 || ^7.5.13" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3-dev" - } - }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -2251,7 +2464,7 @@ "check", "validate" ], - "time": "2018-01-29T19:49:41+00:00" + "time": "2019-11-24T13:36:37+00:00" }, { "name": "zendframework/zend-coding-standard", @@ -2284,55 +2497,184 @@ }, { "name": "zendframework/zend-diactoros", - "version": "1.7.1", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/zendframework/zend-diactoros.git", - "reference": "bf26aff803a11c5cc8eb7c4878a702c403ec67f1" + "reference": "de5847b068362a88684a55b0dbb40d85986cfa52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/bf26aff803a11c5cc8eb7c4878a702c403ec67f1", - "reference": "bf26aff803a11c5cc8eb7c4878a702c403ec67f1", + "url": "https://api.github.com/repos/zendframework/zend-diactoros/zipball/de5847b068362a88684a55b0dbb40d85986cfa52", + "reference": "de5847b068362a88684a55b0dbb40d85986cfa52", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0", + "php": "^7.1", + "psr/http-factory": "^1.0", "psr/http-message": "^1.0" }, "provide": { + "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, "require-dev": { + "ext-curl": "*", "ext-dom": "*", "ext-libxml": "*", - "phpunit/phpunit": "^5.7.16 || ^6.0.8", - "zendframework/zend-coding-standard": "~1.0" + "http-interop/http-factory-tests": "^0.5.0", + "php-http/psr7-integration-tests": "dev-master", + "phpunit/phpunit": "^7.0.2", + "zendframework/zend-coding-standard": "~1.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.7.x-dev", - "dev-develop": "1.8.x-dev" + "dev-master": "2.1.x-dev", + "dev-develop": "2.2.x-dev", + "dev-release-1.8": "1.8.x-dev" } }, "autoload": { + "files": [ + "src/functions/create_uploaded_file.php", + "src/functions/marshal_headers_from_sapi.php", + "src/functions/marshal_method_from_sapi.php", + "src/functions/marshal_protocol_version_from_sapi.php", + "src/functions/marshal_uri_from_sapi.php", + "src/functions/normalize_server.php", + "src/functions/normalize_uploaded_files.php", + "src/functions/parse_cookie_header.php" + ], "psr-4": { "Zend\\Diactoros\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-2-Clause" + "BSD-3-Clause" ], "description": "PSR HTTP Message implementations", - "homepage": "https://github.com/zendframework/zend-diactoros", "keywords": [ "http", "psr", "psr-7" ], - "time": "2018-02-26T15:44:50+00:00" + "time": "2019-11-13T19:16:13+00:00" + }, + { + "name": "zendframework/zend-servicemanager", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-servicemanager.git", + "reference": "a1ed6140d0d3ee803fec96582593ed024950067b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-servicemanager/zipball/a1ed6140d0d3ee803fec96582593ed024950067b", + "reference": "a1ed6140d0d3ee803fec96582593ed024950067b", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "php": "^5.6 || ^7.0", + "psr/container": "^1.0", + "zendframework/zend-stdlib": "^3.2.1" + }, + "provide": { + "container-interop/container-interop-implementation": "^1.2", + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6.5", + "ocramius/proxy-manager": "^1.0 || ^2.0", + "phpbench/phpbench": "^0.13.0", + "phpunit/phpunit": "^5.7.25 || ^6.4.4", + "zendframework/zend-coding-standard": "~1.0.0" + }, + "suggest": { + "ocramius/proxy-manager": "ProxyManager 1.* to handle lazy initialization of services", + "zendframework/zend-stdlib": "zend-stdlib ^2.5 if you wish to use the MergeReplaceKey or MergeRemoveKey features in Config instances" + }, + "bin": [ + "bin/generate-deps-for-config-factory", + "bin/generate-factory-for-class" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.3-dev", + "dev-develop": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\ServiceManager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Factory-Driven Dependency Injection Container", + "keywords": [ + "PSR-11", + "ZendFramework", + "dependency-injection", + "di", + "dic", + "service-manager", + "servicemanager", + "zf" + ], + "time": "2018-12-22T06:05:09+00:00" + }, + { + "name": "zendframework/zend-stdlib", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/zendframework/zend-stdlib.git", + "reference": "66536006722aff9e62d1b331025089b7ec71c065" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zendframework/zend-stdlib/zipball/66536006722aff9e62d1b331025089b7ec71c065", + "reference": "66536006722aff9e62d1b331025089b7ec71c065", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpbench/phpbench": "^0.13", + "phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.2", + "zendframework/zend-coding-standard": "~1.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev", + "dev-develop": "3.3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Zend\\Stdlib\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "SPL extensions, array utilities, error handlers, and more", + "keywords": [ + "ZendFramework", + "stdlib", + "zf" + ], + "time": "2018-08-28T21:34:05+00:00" } ], "aliases": [], diff --git a/data/oauth2.sql b/data/oauth2.sql index 508e667..f971783 100644 --- a/data/oauth2.sql +++ b/data/oauth2.sql @@ -1,66 +1,90 @@ -CREATE TABLE oauth_auth_codes ( - id VARCHAR(100), - user_id INTEGER, - client_id INTEGER, - scopes TEXT NULL, - revoked BOOLEAN, - expires_at TIMESTAMP NULL, - PRIMARY KEY(id) -); +-- +-- Table structure for table `oauth_access_tokens` +-- -CREATE TABLE oauth_access_tokens ( - id VARCHAR(100), - user_id VARCHAR(40) NULL, - client_id VARCHAR(40), - name VARCHAR(255) NULL, - scopes TEXT NULL, - revoked BOOLEAN, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - expires_at TIMESTAMP NULL, - PRIMARY KEY(id) +CREATE TABLE `oauth_access_tokens` ( + `id` varchar(100) PRIMARY KEY NOT NULL, + `user_id` int(10) DEFAULT NULL, + `client_id` int(10) NOT NULL, + `name` varchar(255) DEFAULT NULL, + `scopes` text, + `revoked` tinyint(1) NOT NULL DEFAULT '0', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT NULL, + `expires_at` datetime NOT NULL ); -CREATE INDEX idx1_oauth_access_tokens ON oauth_access_tokens(user_id); - -CREATE TABLE oauth_refresh_tokens ( - id VARCHAR(100), - access_token_id VARCHAR(100), - revoked BOOLEAN, - expires_at TIMESTAMP NULL, - PRIMARY KEY(id) + +CREATE INDEX `IDX_CA42527CA76ED39519EB6921BDA26CCD` ON oauth_access_tokens (`user_id`,`client_id`); +CREATE INDEX `IDX_CA42527CA76ED395` ON oauth_access_tokens (`user_id`); +CREATE INDEX `IDX_CA42527C19EB6921` ON oauth_access_tokens (`client_id`); + +-- +-- Table structure for table `oauth_auth_codes` +-- + +CREATE TABLE `oauth_auth_codes` ( + `id` varchar(100) PRIMARY KEY NOT NULL, + `user_id` int(10) DEFAULT NULL, + `client_id` int(10) NOT NULL, + `scopes` text, + `revoked` tinyint(1) NOT NULL DEFAULT '0', + `expires_at` datetime DEFAULT NULL ); -CREATE INDEX idx1_oauth_refresh_tokens ON oauth_refresh_tokens(access_token_id); - -CREATE TABLE oauth_clients ( - name VARCHAR(40) NOT NULL, - user_id INTEGER NULL, - secret VARCHAR(100) NULL, - redirect VARCHAR(255), - personal_access_client BOOLEAN, - password_client BOOLEAN, - revoked BOOLEAN, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL, - PRIMARY KEY (name) + +CREATE INDEX `IDX_BB493F83A76ED395` ON oauth_auth_codes (`user_id`); +CREATE INDEX `IDX_BB493F8319EB6921` ON oauth_auth_codes (`client_id`); + +-- +-- Table structure for table `oauth_clients` +-- + +CREATE TABLE `oauth_clients` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` int(10) DEFAULT NULL, + `name` varchar(100) NOT NULL, + `secret` varchar(100) DEFAULT NULL, + `redirect` varchar(255) DEFAULT NULL, + `personal_access_client` tinyint(1) DEFAULT NULL, + `password_client` tinyint(1) DEFAULT NULL, + `revoked` tinyint(1) DEFAULT NULL, + `created_at` datetime DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime DEFAULT NULL ); -CREATE INDEX idx1_oauth_clients ON oauth_clients(user_id); -CREATE TABLE oauth_personal_access_clients ( - client_id INTEGER, - created_at TIMESTAMP NULL, - updated_at TIMESTAMP NULL +CREATE INDEX `IDX_13CE81015E237E06A76ED395BDA26CCD` ON oauth_clients (`name`,`user_id`); +CREATE INDEX `IDX_13CE8101A76ED395` ON oauth_clients (`user_id`); + +-- +-- Table structure for table `oauth_refresh_tokens` +-- + +CREATE TABLE `oauth_refresh_tokens` ( + `id` varchar(100) PRIMARY KEY NOT NULL, + `access_token_id` varchar(100) NOT NULL, + `revoked` tinyint(1) NOT NULL DEFAULT '0', + `expires_at` datetime NOT NULL ); -CREATE INDEX idx1_oauth_personal_access_clients ON oauth_personal_access_clients(client_id); - -CREATE TABLE oauth_users ( - username VARCHAR(40) NOT NULL, - password VARCHAR(100) NOT NULL, - first_name VARCHAR(80), - last_name VARCHAR(80), - PRIMARY KEY (username) + +CREATE INDEX `IDX_5AB6872CCB2688BDA26CCD` ON oauth_refresh_tokens (`access_token_id`); + +-- +-- Table structure for table `oauth_scopes` +-- + +CREATE TABLE `oauth_scopes` ( + `id` varchar(100) PRIMARY KEY NOT NULL ); -CREATE TABLE oauth_scopes ( - id VARCHAR(30) NOT NULL, - PRIMARY KEY (id) +-- +-- Table structure for table `oauth_users` +-- + +CREATE TABLE `oauth_users` ( + `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + `username` varchar(320) UNIQUE NOT NULL, + `password` varchar(100) NOT NULL, + `first_name` varchar(80) DEFAULT NULL, + `last_name` varchar(80) DEFAULT NULL ); + +CREATE INDEX `UNIQ_93804FF8F85E0677` ON oauth_users (`username`); diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ec1829f..36c9f2a 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -7,7 +7,7 @@ read/subscribe to the following resources: - [Coding Standards](https://github.com/zendframework/zend-coding-standard) - [Forums](https://discourse.zendframework.com/c/contributors) - - [Slack](https://zendframework-slack.herokuapp.com) + - [Chat](https://zendframework-slack.herokuapp.com) - [Code of Conduct](CODE_OF_CONDUCT.md) If you are working on new features or refactoring diff --git a/docs/ISSUE_TEMPLATE.md b/docs/ISSUE_TEMPLATE.md index eaaad0f..1826652 100644 --- a/docs/ISSUE_TEMPLATE.md +++ b/docs/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ - [ ] I was not able to find an [open](https://github.com/zendframework/zend-expressive-authentication-oauth2/issues?q=is%3Aopen) or [closed](https://github.com/zendframework/zend-expressive-authentication-oauth2/issues?q=is%3Aclosed) issue matching what I'm seeing. - - [ ] This is not a question. (Questions should be asked on [slack](https://zendframework.slack.com/) ([Signup for Slack here](https://zendframework-slack.herokuapp.com/)) or our [forums](https://discourse.zendframework.com/).) + - [ ] This is not a question. (Questions should be asked on [chat](https://zendframework.slack.com/) ([Signup here](https://zendframework-slack.herokuapp.com/)) or our [forums](https://discourse.zendframework.com/).) Provide a narrative description of what you are trying to accomplish. diff --git a/docs/SUPPORT.md b/docs/SUPPORT.md index daa20e7..ae71cc0 100644 --- a/docs/SUPPORT.md +++ b/docs/SUPPORT.md @@ -3,13 +3,13 @@ Zend Framework offers three support channels: - For real-time questions, use our - [Slack](https://zendframework-slack.herokuapp.com) + [chat](https://zendframework-slack.herokuapp.com) - For detailed questions (e.g., those requiring examples) use our [forums](https://discourse.zendframework.com/c/questions/expressive) - To report issues, use this repository's [issue tracker](https://github.com/zendframework/zend-expressive-authentication-oauth2/issues/new) -**DO NOT** use the issue tracker to ask questions; use Slack or the forums for +**DO NOT** use the issue tracker to ask questions; use chat or the forums for that. Questions posed to the issue tracker will be closed. When reporting an issue, please include the following details: diff --git a/docs/book/authorization-server.md b/docs/book/authorization-server.md new file mode 100644 index 0000000..5203650 --- /dev/null +++ b/docs/book/authorization-server.md @@ -0,0 +1,6 @@ + + diff --git a/docs/book/grant/auth_code.md b/docs/book/grant/auth_code.md index d716567..fe37c6b 100644 --- a/docs/book/grant/auth_code.md +++ b/docs/book/grant/auth_code.md @@ -1,76 +1,6 @@ -# Authorization code - -The authorization code is used to authenticate a web application with a -third-party service (e.g., imagine you built a web application that needs to -consume the API of Facebook). You can authenticate your application using the -third-party server with a 4-step flow as illustrated in this diagram: - -![Authorization code diagram](auth_code.png) - -The web application sends a request (including the `client_id` and the -`redirect_uri`) to the authorization server asking for an Authorization code (1). -The authorization server shows an Allow/Deny page to the end-user requesting -authorization for access. If the user clicks on "Allow", the server sends the -authorization code to the web application using the `redirect_uri` (2). -The web application can now perform a token request, passing the `client_id`, -the `redirect_uri`, the `client_secret`, and the authentication code to prove -that it is authorized to perform the request (3). The authorization server sends -the access token in response if the request is valid (4). - -## Request the authorization code - -The client sends the following parameters via query string arguments to the -authorization server: - -- `response_type` = code. -- `client_id` with the client identifer. -- `redirect_uri` with the URI to which to redirect the client following - successful authorization. This parameter is optional, but if it is not sent, - the user will be redirected to a default location on completion. -- `scope` with a space-delimited list of requested scope permissions. -- `state` with a Cross-Site Request Forgery (CSRF) token. This parameter is - optional, but highly recommended. You can store the value of the CSRF token in - the user’s session to be validated in the next step. - -The user will then be asked to login to the authorization server and approve the -client request. If the user approves the request they will be redirected to the -redirect URI with the following parameters in the query string arguments: - -- `code` with the authorization code. -- `state` with the CSRF parameter sent in the original request. You can compare - this value with the one stored in the user’s session. - -## Request the access token - -The client sends a POST request to the authorization server with the following -parameters: - -- `grant_type` = authorization_code. -- `client_id` with the client’s ID. -- `client_secret` with the client’s secret. -- `redirect_uri` with the previous client redirect URI. -- `code` with the authorization code as returned in the authorization code - request (as detailed in the previous section). - -The authorization server responds with a JSON payload similar to the following: - -```json -{ - "token_type" : "Bearer", - "expires_in" : "3600", - "refresh_token" : "YWYwNjhmNmZmMDhmZjkyOGJj...", - "access_token" : "eyJ0eXAiOiJKV1Q..." -} -``` - -The values are as follows: - -- The `token_type` is the type of generated token (here, and generally, - "Bearer"). -- The `expires_in` value is an integer representing the time-to-live (in - seconds) of the access token. -- The `refresh_token` is a token that can be used to refresh the `access_token` - when expired. -- The `access_token` contains a JSON Web Token (JWT) signed with the - authorization server’s private key. This token must be used in the - `Authorization` request HTTP header on subsequent requests. + + diff --git a/docs/book/grant/client_credentials.md b/docs/book/grant/client_credentials.md index d7e9969..f63f96a 100644 --- a/docs/book/grant/client_credentials.md +++ b/docs/book/grant/client_credentials.md @@ -1,31 +1,6 @@ -# Client credentials - -The client credentials grant is used in machine-to-machine scenarios: for -example, a client making API requests that do not require a user's permission. - -The client sends a `POST` request with following body parameters to the -authorization server: - -- `grant_type` = client_credentials. -- `client_id` with the client's ID. -- `client_secret` with the client's secret. -- `scope` with a space-delimited list of requested scope permissions. - -The authorization server responds with a JSON payload as follows: - -```json -{ - "token_type" : "Bearer", - "expires_in" : "3600", - "access_token" : "eyJ0eXAiOiJKV1Q..." -} -``` - -The values returned are as follows: - -- The `token_type` is the type of generated token (here, and generally, Bearer). -- `expires_in` is an integer representing the time-to-live (in seconds) of the - access token. -- The `access_token` contains a JSON Web Token (JWT) signed with the - authorization server’s private key. This token must be used in the - `Authorization` request HTTP header in subsequent requests. + + diff --git a/docs/book/grant/implicit.md b/docs/book/grant/implicit.md index 5ca2d4c..06419ca 100644 --- a/docs/book/grant/implicit.md +++ b/docs/book/grant/implicit.md @@ -1,35 +1,6 @@ -# Implicit grant - -The implicit grant is similar to the [authorization code](auth_code.md) grant, -with two differences: it's used for user-agent-based clients (e.g. single page -applications) that cannot store a secret in a secure way; additionally, the -authorization server returns the access token directly, without the need of an -authorization code. - -The client sends the following parameter via a query string argument to the -authorization server: - -- `response_type` = token. -- `client_id`, with the client’s ID. -- `redirect_uri`, with the URI to which to redirect the client after completing - authorization. This parameter is optional; if not provided, however, the user - will be redirected to a default location. -- `scope`, with a space-delimited list of requested scope permissions. -- `state`, with a Cross-Site Request Forgery (CSRF) token. This parameter is - optional but highly recommended. You can store the value of CSRF token in the - user’s session to be validated in the next step. - -The user will then be asked to login to the authorization server and approve the -client request. If the user approves the request they will be redirected to the -redirect URI with the following parameters in the query string arguments: - -- `token_type` = Bearer. -- `expires_in`, an integer representing the time-to-live (in seconds) of the - access token. -- `access_token`, the access token represented by a JSON Web Token (JWT) signed - with the authorization server’s private key. -- `state`, with the CSRF parameter sent in the original request. You can compare - this value with the one stored in the user’s session. - -Refresh tokens are not to be issued for `implicit` grants. This is a security -restriction coming from the OAuth2 specification, [RFC 6749](https://tools.ietf.org/html/rfc6749#page-35). + + diff --git a/docs/book/grant/password.md b/docs/book/grant/password.md index 30f7f69..938dc84 100644 --- a/docs/book/grant/password.md +++ b/docs/book/grant/password.md @@ -1,34 +1,6 @@ -# Password - -This use case can be used to authenticate an API with user's password grant. -The typical scenario includes a Login web page with username and password that -is used to authenticate against a first-party API. Password grant is only -appropriate for **trusted clients**. If you build your own website as a client -of your API, then this is a great way to handle logging in. - -The client sends a POST request with following parameters: - -- `grant_type` = password; -- `client_id` with the client’s ID; -- `client_secret` with the client’s secret; -- `scope` with a space-delimited list of requested scope permissions; -- `username` with the user’s username; -- `password` with the user’s password. - -The authorization server responds with a JSON as follows: - -```json -{ - "token_type" : "Bearer", - "expires_in" : "3600", - "refresh_token" : "YWYwNjhmNmZmMDhmZjkyOGJj...", - "access_token" : "eyJ0eXAiOiJKV1Q..." -} -``` - -The `token_type` is the type of generated token (Bearer). The `expires_in` is -an integer representing the TTL (in seconds) of the access token. -The `refresh_token` a token that can be used to refresh the `access_token` when -expired. -The `access_token` contains a `JWT` signed with the authorization server’s -private key. This token must be used in the `Authorization` request HTTP header. + + diff --git a/docs/book/grant/refresh_token.md b/docs/book/grant/refresh_token.md index aab0ccd..24a8ddc 100644 --- a/docs/book/grant/refresh_token.md +++ b/docs/book/grant/refresh_token.md @@ -1,39 +1,6 @@ -# Refresh token - -The OAuth2 framework provides the ability to _refresh_ the access token, -generating a new one with a new lifetime. This action can be performed using -the `refresh_token` value, if present in the access token response. - -To request a token refresh, the client needs to send a `POST` request with -the following parameters: - -- `grant_type` = refresh_token. -- `refresh_token` with the refresh token. -- `client_id` with the client’s ID. -- `client_secret` with the client’s secret. -- `scope` with a space-delimited list of requested scope permissions. This is - optional; if not sent, the original scopes will be used. Otherwise you can - request a _reduced_ scope; you may never _expand_ scope during a refresh - operation. - -The authorization server responds with a JSON payload as follows: - -```json -{ - "token_type" : "Bearer", - "expires_in" : "3600", - "refresh_token" : "YWYwNjhmNmZmMDhmZjkyOGJj...", - "access_token" : "eyJ0eXAiOiJKV1Q..." -} -``` - -The values are as follows: - -- The `token_type` is the type of generated token (here, and generally, Bearer). -- `expires_in` is an integer representing the time-to-live (in seconds) of the - access token. -- The `refresh_token` a token that can be used to refresh the `access_token` - when expired. -- The `access_token` contains a JSON Web Token (JWT) signed with the - authorization server’s private key. This token must be used in the - `Authorization` request HTTP header on all subsequent requests. + + diff --git a/docs/book/index.html b/docs/book/index.html deleted file mode 100644 index 6ec4e7c..0000000 --- a/docs/book/index.html +++ /dev/null @@ -1,9 +0,0 @@ -
-
-

zendframework/zend-expressive-authentication-oauth2

- -

OAuth2 (server) for Expressive and PSR-7 applications.

- -
$ composer require zendframework/zend-expressive-authentication-oauth2
-
-
diff --git a/docs/book/intro.md b/docs/book/intro.md index e7747cc..8edcf57 100644 --- a/docs/book/intro.md +++ b/docs/book/intro.md @@ -1,134 +1,6 @@ -# zendframework/zend-expressive-authentication-oauth2 - -This component provides [OAuth2](https://oauth.net/2/) (server) authentication -for [Expressive](https://docs.zendframework.com/zend-expressive/) and -[PSR-7](https://www.php-fig.org/psr/psr-7/) applications. It implements -`Zend\Expressive\Authentication\AuthenticationInterface`, and it be used as -an adapter for [zend-expressive-authentication](https://github.com/zendframework/zend-expressive-authentication). - -This library uses the [league/oauth2-server](https://oauth2.thephpleague.com/) -package for implementing the OAuth2 server. - -If you need an introduction to OAuth2, you can read the following references: - -- [OAuth2 documentation](https://apigility.org/documentation/auth/authentication-oauth2) - from the Apigility project. -- [An Introduction to OAuth 2](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2) - by Digital Ocean. -- The [OAuth2 specification](https://oauth.net/2/) itself, via its official - website. - -## Installation - -In order to implement the OAuth2 server, we first need to configure it. The -first step is to generate new cryptographic keys. We need to execute the script -`bin/generate-oauth2-keys` in order to generate these keys. - -```bash -$ ./vendor/bin/generate-oauth2-keys -``` - -This script will store the keys in the parent application `data` folder if found: - -``` -Private key stored in: -./data/oauth/private.key -Public key stored in: -./data/oauth/public.key -Encryption key stored in: -./data/oauth/encryption.key -``` - -The script will generate public and private keys, and an encryption key. -These keys are used by [league/oauth2-server](https://oauth2.thephpleague.com/) -as security settings for the OAuth2 server infrastructure. - -## Configuration - -The OAuth2 server is configured by the `authentication` configuration key in the -PSR-11 container (e.g. [zend-servicemanager](https://github.com/zendframework/zend-servicemanager)). - -The default values are: - -```php -return [ - 'private_key' => __DIR__ . '/../data/oauth/private.key', - 'public_key' => __DIR__ . '/../data/oauth/public.key', - 'encryption_key' => require __DIR__ . '/../data/oauth/encryption.key', - 'access_token_expire' => 'P1D', - 'refresh_token_expire' => 'P1M', - 'auth_code_expire' => 'PT10M', - 'pdo' => [ - 'dsn' => '', - 'username' => '', - 'password' => '' - ] -]; -``` - -The `private_key` and `public_key` values contains the paths to the previous -generated pair of keys. The `encryption_key` contains the encryption key value -as a string, as stored in the `data/oauth/encryption.key` file. - -The `access_token_expire` value is the time-to-live (TTL) value of the access -token. The time period is represented using the [DateInterval](http://php.net/manual/en/class.dateinterval.php) -format in PHP. The default value is `P1D` (1 day). - -The `refresh_token_expire` value is the TTL used for the refresh token. The -default value is 1 month. - -The `auth_code_expire` value is th TTL of the authentication code, used in -the [authorization code grant](https://oauth2.thephpleague.com/authorization-server/auth-code-grant/) -scenario. The default value is 10 minutes. - -The last parameter is the PDO database configuration. Here we need to insert -the parameters to access the OAuth2 database. These parameters are the `dsn`, -the `username`, and the `password`, if required. The SQL structure of this -database is stored in the [data/oauth2.sql](https://github.com/zendframework/zend-expressive-authentication-oauth2/blob/master/data/oauth2.sql) -file. - -You need to provide an OAuth2 database yourself, or generate a [SQLite](https://www.sqlite.org) -database with the following command (using `sqlite3` for GNU/Linux): - -```bash -$ sqlite3 data/oauth2.sqlite < data/oauth2.sql -``` - -You can also create some testing values using the `data/oauth2_test.sql` file: - -```bash -$ sqlite3 data/oauth2.sqlite < data/oauth2_test.sql -``` - -These commands will insert the following testing values: - -- a client `client_test` with secret `test`, used for [client_credentials](grant/client_credentials.md) - and the [password](grant/password.md) grant type. -- a client `client_test2` with secret `test`, used for [authorization code](grant/auth_code.md) - and [implicit](grant/implicit.md) grant type. -- a user `user_test` with password `test`. -- a `test` scope. - -For security reason, the client `secret` and the user `password` are stored -using the `bcrypt` algorithm provided by [password_hash](http://php.net/manual/en/function.password-hash.php) -function of PHP. - -## Configure the OAuth2 route - -As last step, in order to use the OAuth2 server you need to configure a route -to point to `Zend\Expressive\Authentication\OAuth2\OAuth2Middleware` with -GET and POST HTTP verbs. - -For instance, you can add the following route to your Expressive application: - -```php -$app->route( - '/oauth', - Zend\Expressive\Authentication\OAuth2\OAuth2Middleware::class, - ['GET', 'POST'], - 'oauth' -); -``` - -With this configuration, you can interact with the OAuth2 server using `/oauth` -URL. + + diff --git a/docs/book/usage.md b/docs/book/usage.md index 1d34e15..fe265d8 100644 --- a/docs/book/usage.md +++ b/docs/book/usage.md @@ -1,55 +1,6 @@ -# Usage - -If you successfully configured the OAuth2 server as detailed in the -[installation](intro.md) section, you can request an access token using the -OAuth2 server route defined [before](intro.md#configure-the-oauth2-route) -(e.g. `/oauth`). - -You can require an access token using one of the following scenarios: - -- [client credentials](grant/client_credentials.md); -- [password](grant/password.md); -- [authorization code](grant/auth_code.md); -- [implicit](grant/implicit.md); -- [refresh token](grant/refresh_token.md). - -## Authenticate a middleware - -This library uses the authentication abstraction of the `Zend\Expressive\Authentication\AuthenticationMiddleware` -class provided by [zend-expressive-authentication](https://github.com/zendframework/zend-expressive-authentication). - -In order to use OAuth2 we need to configure the service -`Zend\Expressive\Authentication\AuthenticationInterface` to resolve in -`Zend\Expressive\Authentication\OAuth2\OAuth2Adapter`. Using the -[zend-servicemanager](https://github.com/zendframework/zend-servicemanager) this -can be achieved using `aliases` with the following configuration: - -```php -use Zend\Expressive\Authentication; - -return [ - 'dependencies' => [ - 'aliases' => [ - Authentication\AuthenticationInterface::class => Authentication\OAuth2\OAuth2Adapter::class, - ], - ], -]; -``` - -The previous configuration will instruct `zend-expressive-authentication` to use -the OAuth2 adapter. This adapter does not require a `Zend\Expressive\Authentication\UserRepositoryInterface`. -The OAuth2 database with user and client credentials is managed by the component -itself. - -When the service alias is configured, you can immediately begin authenticating -your application/API by adding the `AuthenticationMiddleware` to either your -application or route-specific middleware pipeline. For instance, using an -[Expressive](https://docs.zendframework.com/zend-expressive/) application, you -could add it to a specific route, as follows: - -```php -$app->post('/api/users', [ - Zend\Expressive\Authentication\AuthenticationMiddleware::class, - App\Action\AddUserAction::class, -], 'api.add.user'); -``` + + diff --git a/docs/book/v1/authenticated-user.md b/docs/book/v1/authenticated-user.md new file mode 100644 index 0000000..c717749 --- /dev/null +++ b/docs/book/v1/authenticated-user.md @@ -0,0 +1,97 @@ +# Authenticated user + +Once the user is authenticated, zend-expressive-authentication-oauth2 stores the +user's authorization details in a PSR-7 attribute under the name +`Zend\Expressive\Authentication\UserInterface`, using an object implementing the +[interface of the same name](https://github.com/zendframework/zend-expressive-authentication/blob/master/src/UserInterface.php). + +This object contains all information discovered and/or generated by +[thephpleage/oauth2-server](https://oauth2.thephpleague.com), including the +following data: + +```php +[ + 'oauth_user_id' => /* user's identifier (string) */, + 'oauth_client_id' => /* the client id (string) */, + 'oauth_access_token_id' => /* the access token id (string) */, + 'oauth_scopes' => /* the scopes (mixed, usually an array) */ +] +``` + +You may retrieve all of these values using the `getDetails()` method, or +individually using the `getDetail($name)` method, of the user instance. As an +example: + +```php +$user->getDetails(); // returns all the values, as array + +// Retrieves only the oauth_user_id value, using a default of boolean false: +$userId = $user->getDetail('oauth_user_id', false); +if (false !== $userId) { + printf("The user ID is %s", $userId); +} +``` + +If you want to retrieve the identity of the user (or the client), you can also +use the `getIdentity()` method. This method returns the user's ID if it is +not null; otherwise it returns the client's ID. + +**The `getRoles()` method of the user instance always returns an empty array.** + +## Customize the user instance + +If you wish to provide a custom `Zend\Expressive\Authentication\UserInterface` +implementation, you will need to provide: + +- a custom implementation of the the interface. +- a factory capable of generating instances of that implementation. +- a DI factory for generating the previous factory. +- configuration wiring the `UserInterface` service to your factory. + +The factory noted in the second step should be a callable with the following +signature: + +```php +function ( + string $identity, + array $roles = [], + array $details = [] +) : Zend\Expressive\Authentication\UserInterface +``` + +As an example of the factory in the third point, you will create a standard DI +factory to return it. It could, for instance, compose a database adapter to pull +information and create your custom user implementation: + +```php +use Psr\Container\ContainerInterface; +use Zend\Db\Adapter\AdapterInterface as DbAdapter; +use Zend\Expressive\Authentication\UserInterface; + +class CustomUserFactory +{ + public function __invoke(ContainerInterface $container) : callable + { + $db = $container->get(DbAdapter::class); + return function (string $identity, array $roles = [], array $details = []) use ($db) : UserInterface { + // get some data from $db + // return a new instance + return new MyCustomUserType(/* ... */); + }); + } +} +``` + +You will then need to wire this factory to the `UserInterface` service, +per the following example: + +```php +// config/autoload/custom-user.local.php +return [ + 'dependencies' => [ + 'factories' => [ + UserInterface::class => CustomUserFactory::class, + ], + ], +]; +``` diff --git a/docs/book/v1/authorization-server.md b/docs/book/v1/authorization-server.md new file mode 100644 index 0000000..1981f87 --- /dev/null +++ b/docs/book/v1/authorization-server.md @@ -0,0 +1,119 @@ +# Implement an authorization server + +This library provides the basics for implementing an authorization server +for your application. + +Since there are authorization flows that require user interaction, +**your application is expected to provide the middleware to handle this**. + +## Add the token endpoint + +Adding the token endpoint involves routing to the provided +`Zend\Expressive\Authentication\OAuth2\TokenEndpointHandler`. + +This endpoint **MUST** accept `POST` requests. + +For example: + +```php +use Zend\Expressive\Authentication\OAuth2; + +$app->post('/oauth2/token', OAuth2\TokenEndpointHandler::class); +``` + +## Add the authorization endpoint + +The authorization endpoint is the URL to which the client redirects +to obtain an access token or authorization code. + +This endpoint **MUST** accept `GET` requests and should: + +- Validate the request (especially for a valid client id and redirect url). + +- Make sure the user is authenticated (for example, by showing a login + prompt if needed). + +- Optionally, request the user's consent to grant access to the client. + +- Redirect to a specified url of the client with success or error information. + +The first and the last items are provided by this library. + +For example, to add the authorization endpoint, you can declare a middleware +pipeline for the route as follows: + +```php +use Zend\Expressive\Authentication\OAuth2; +use Zend\Expressive\Session\SessionMiddleware; + +$app->route('/oauth2/authorize', [ + SessionMiddleware::class, + + OAuth2\AuthorizationMiddleware::class, + + // The following middleware is provided by your application (see below): + App\OAuthAuthorizationMiddleware::class, + + OAuth2\AuthorizationHandler::class +], ['GET', 'POST']); +``` + +In your `App\OAuthAuthorizationMiddleware`, you'll have access to the +`League\OAuth2\Server\RequestTypes\AuthorizationRequest` via the PSR-7 request. +Your middleware should populate the `AuthorizationRequest`'s user entity via its +`setUser()` method, and the user's consent decision via the +`setAuthorizationApproved()`method. + +As an example: + +```php +namespace App; + +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Psr\Http\Message\ResponseInterface; +use League\OAuth2\Server\RequestTypes\AuthorizationRequest; +use Zend\Expressive\Authentication\UserInterface; + +class OAuthAuthorizationMiddleware implements MiddlewareInterface +{ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + // Assume a middleware handled the authentication check and + // populates the user object, which also implements the + // OAuth2 UserEntityInterface + $user = $request->getAttribute(UserInterface::class); + + // Assume the SessionMiddleware handles and populates a session + // container + $session = $request->getAttribute('session'); + + // This is populated by the previous middleware: + /** @var AuthorizationRequest $authRequest */ + $authRequest = $request->getAttribute(AuthorizationRequest::class); + + // The user is authenticated: + if ($user) { + $authRequest->setUser($user); + + // This assumes all clients are trusted, but you could + // handle consent here, or within the next middleware + // as needed. + $authRequest->setAuthorizationApproved(true); + + return $handler->handle($request); + } + + // The user is not authenticated, show login form ... + + // Store the auth request state + // NOTE: Do not attempt to serialize or store the authorization + // request object. Store the query parameters instead and redirect + // with these to this endpoint again to replay the request. + $session['oauth2_request_params'] = $request->getQueryParams(); + + return new RedirectResponse('/oauth2/login'); + } +} +``` diff --git a/docs/book/v1/grant/auth_code.md b/docs/book/v1/grant/auth_code.md new file mode 100644 index 0000000..33733bc --- /dev/null +++ b/docs/book/v1/grant/auth_code.md @@ -0,0 +1,76 @@ +# Authorization code + +The authorization code is used to authenticate a web application with a +third-party service (e.g., imagine you built a web application that needs to +consume the API of Facebook). You can authenticate your application using the +third-party server with a 4-step flow as illustrated in this diagram: + +![Authorization code diagram](auth_code.png) + +The web application sends a request (including the `client_id` and the +`redirect_uri`) to the authorization server asking for an authorization code (1). +The authorization server shows an allow/deny page to the end-user requesting +authorization for access. If the user clicks on "Allow", the server sends the +authorization code to the web application using the `redirect_uri` (2). +The web application can now perform a token request, passing the `client_id`, +the `redirect_uri`, the `client_secret`, and the authentication code to prove +that it is authorized to perform the request (3). The authorization server sends +the access token in the response if the request is valid (4). + +## Request the authorization code + +The client sends the following parameters via query string arguments to the +authorization server: + +- `response_type` = code. +- `client_id` with the client identifer. +- `redirect_uri` with the URI to which to redirect the client following + successful authorization. This parameter is optional, but if it is not sent, + the user will be redirected to a default location on completion. +- `scope` with a space-delimited list of requested scope permissions. +- `state` with a Cross-Site Request Forgery (CSRF) token. This parameter is + optional, but highly recommended. You can store the value of the CSRF token in + the user’s session to be validated in the next step. + +The user will then be asked to login to the authorization server and approve the +client request. If the user approves the request they will be redirected to the +redirect URI with the following parameters in the query string arguments: + +- `code` with the authorization code. +- `state` with the CSRF parameter sent in the original request. You can compare + this value with the one stored in the user’s session. + +## Request the access token + +The client sends a POST request to the authorization server with the following +parameters: + +- `grant_type` = authorization_code. +- `client_id` with the client’s ID. +- `client_secret` with the client’s secret. +- `redirect_uri` with the previous client redirect URI. +- `code` with the authorization code as returned in the authorization code + request (as detailed in the previous section). + +The authorization server responds with a JSON payload similar to the following: + +```json +{ + "token_type" : "Bearer", + "expires_in" : "3600", + "refresh_token" : "YWYwNjhmNmZmMDhmZjkyOGJj...", + "access_token" : "eyJ0eXAiOiJKV1Q..." +} +``` + +The values are as follows: + +- The `token_type` is the type of generated token (here, and generally, + "Bearer"). +- The `expires_in` value is an integer representing the time-to-live (in + seconds) of the access token. +- The `refresh_token` is a token that can be used to refresh the `access_token` + when expired. +- The `access_token` contains a JSON Web Token (JWT) signed with the + authorization server’s private key. This token must be used in the + `Authorization` request HTTP header on subsequent requests. diff --git a/docs/book/grant/auth_code.png b/docs/book/v1/grant/auth_code.png similarity index 100% rename from docs/book/grant/auth_code.png rename to docs/book/v1/grant/auth_code.png diff --git a/docs/book/v1/grant/client_credentials.md b/docs/book/v1/grant/client_credentials.md new file mode 100644 index 0000000..20e1ac5 --- /dev/null +++ b/docs/book/v1/grant/client_credentials.md @@ -0,0 +1,32 @@ +# Client credentials + +The client credentials grant is used in machine-to-machine scenarios. For +example, you would use it with a client making API requests that do not require +a user's permission. + +The client sends a `POST` request with the following body parameters to the +authorization server: + +- `grant_type` = client_credentials. +- `client_id` with the client's ID. +- `client_secret` with the client's secret. +- `scope` with a space-delimited list of requested scope permissions. + +The authorization server responds with a JSON payload as follows: + +```json +{ + "token_type" : "Bearer", + "expires_in" : "3600", + "access_token" : "eyJ0eXAiOiJKV1Q..." +} +``` + +The values returned are as follows: + +- The `token_type` is the type of generated token (here, and generally, Bearer). +- `expires_in` is an integer representing the time-to-live (in seconds) of the + access token. +- The `access_token` contains a JSON Web Token (JWT) signed with the + authorization server’s private key. This token must be used in the + `Authorization` request HTTP header in subsequent requests. diff --git a/docs/book/v1/grant/implicit.md b/docs/book/v1/grant/implicit.md new file mode 100644 index 0000000..b8c162c --- /dev/null +++ b/docs/book/v1/grant/implicit.md @@ -0,0 +1,35 @@ +# Implicit grant + +The implicit grant is similar to the [authorization code](auth_code.md) grant, +with two differences: it's used for user-agent-based clients (e.g. single page +applications) that cannot store a secret in a secure way; additionally, the +authorization server returns the access token directly, without the need of an +authorization code. + +The client sends the following parameter via a query string argument to the +authorization server: + +- `response_type` = token. +- `client_id`, with the client’s ID. +- `redirect_uri`, with the URI to which to redirect the client after completing + authorization. This parameter is optional; if not provided, however, the user + will be redirected to a default location. +- `scope`, with a space-delimited list of requested scope permissions. +- `state`, with a Cross-Site Request Forgery (CSRF) token. This parameter is + optional but highly recommended. You can store the value of CSRF token in the + user’s session to be validated in the next step. + +The user will then be asked to login to the authorization server and approve the +client request. If the user approves the request, they will be redirected to the +redirect URI with the following parameters in the query string arguments: + +- `token_type` = Bearer. +- `expires_in`, an integer representing the time-to-live (in seconds) of the + access token. +- `access_token`, the access token represented by a JSON Web Token (JWT) signed + with the authorization server’s private key. +- `state`, with the CSRF parameter sent in the original request. You can compare + this value with the one stored in the user’s session. + +Refresh tokens are not to be issued for `implicit` grants. This is a security +restriction coming from the OAuth2 specification, [RFC 6749](https://tools.ietf.org/html/rfc6749#page-35). diff --git a/docs/book/v1/grant/password.md b/docs/book/v1/grant/password.md new file mode 100644 index 0000000..a2101f5 --- /dev/null +++ b/docs/book/v1/grant/password.md @@ -0,0 +1,35 @@ +# Password + +This use case allows authentication to an API using the user's credentials +(generally a username and password). The typical scenario includes a "Login" +web page that captures a username and password combination that is used to +authenticate against a first-party API. Password grant is only appropriate for +**trusted clients**. If you build your own website as a client of your API, then +this is a great way to handle logging in. + +The client sends a POST request with following parameters: + +- `grant_type` = password. +- `client_id` with the client’s ID. +- `client_secret` with the client’s secret. +- `scope` with a space-delimited list of requested scope permissions. +- `username` with the user’s username. +- `password` with the user’s password. + +The authorization server responds with a JSON as follows: + +```json +{ + "token_type" : "Bearer", + "expires_in" : "3600", + "refresh_token" : "YWYwNjhmNmZmMDhmZjkyOGJj...", + "access_token" : "eyJ0eXAiOiJKV1Q..." +} +``` + +The `token_type` is the type of generated token (Bearer). The `expires_in` is an +integer representing the TTL (in seconds) of the access token. The +`refresh_token` a token that can be used to refresh the `access_token` when +expired. The `access_token` contains a `JWT` signed with the authorization +server’s private key. This token must be used in the `Authorization` request +HTTP header. diff --git a/docs/book/v1/grant/refresh_token.md b/docs/book/v1/grant/refresh_token.md new file mode 100644 index 0000000..aab0ccd --- /dev/null +++ b/docs/book/v1/grant/refresh_token.md @@ -0,0 +1,39 @@ +# Refresh token + +The OAuth2 framework provides the ability to _refresh_ the access token, +generating a new one with a new lifetime. This action can be performed using +the `refresh_token` value, if present in the access token response. + +To request a token refresh, the client needs to send a `POST` request with +the following parameters: + +- `grant_type` = refresh_token. +- `refresh_token` with the refresh token. +- `client_id` with the client’s ID. +- `client_secret` with the client’s secret. +- `scope` with a space-delimited list of requested scope permissions. This is + optional; if not sent, the original scopes will be used. Otherwise you can + request a _reduced_ scope; you may never _expand_ scope during a refresh + operation. + +The authorization server responds with a JSON payload as follows: + +```json +{ + "token_type" : "Bearer", + "expires_in" : "3600", + "refresh_token" : "YWYwNjhmNmZmMDhmZjkyOGJj...", + "access_token" : "eyJ0eXAiOiJKV1Q..." +} +``` + +The values are as follows: + +- The `token_type` is the type of generated token (here, and generally, Bearer). +- `expires_in` is an integer representing the time-to-live (in seconds) of the + access token. +- The `refresh_token` a token that can be used to refresh the `access_token` + when expired. +- The `access_token` contains a JSON Web Token (JWT) signed with the + authorization server’s private key. This token must be used in the + `Authorization` request HTTP header on all subsequent requests. diff --git a/docs/book/v1/intro.md b/docs/book/v1/intro.md new file mode 100644 index 0000000..16dbf3b --- /dev/null +++ b/docs/book/v1/intro.md @@ -0,0 +1,205 @@ +# zendframework/zend-expressive-authentication-oauth2 + +This component provides [OAuth2](https://oauth.net/2/) (server) authentication +for [Expressive](https://docs.zendframework.com/zend-expressive/) and +[PSR-7](https://www.php-fig.org/psr/psr-7/)/[PSR-15](https://www.php-fig.org/psr/psr-15/) +applications. It implements `Zend\Expressive\Authentication\AuthenticationInterface`, +and it can be used as an adapter for [zend-expressive-authentication](https://github.com/zendframework/zend-expressive-authentication). + +This library uses the [league/oauth2-server](https://oauth2.thephpleague.com/) +package for implementing the OAuth2 server. + +If you need an introduction to OAuth2, you can read the following references: + +- [OAuth2 documentation](https://apigility.org/documentation/auth/authentication-oauth2) + from the Apigility project. +- [An Introduction to OAuth 2](https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2) + by DigitalOcean. +- The [OAuth2 specification](https://oauth.net/2/) itself, via its official + website. + +## Installation + +In order to implement the OAuth2 server, we first need to configure it. The +first step is to generate new cryptographic keys. We need to execute the script +`./vendor/bin/generate-oauth2-keys` in order to generate these keys. + +```bash +$ ./vendor/bin/generate-oauth2-keys +``` + +This script will store the keys in the application's `data` folder if found: + +```text +Private key stored in: +./data/oauth/private.key +Public key stored in: +./data/oauth/public.key +Encryption key stored in: +./data/oauth/encryption.key +``` + +The script will generate public and private keys, and an encryption key. +These keys are used by [league/oauth2-server](https://oauth2.thephpleague.com/) +as security settings for the OAuth2 server infrastructure. + +## Configuration + +The OAuth2 server is configured by the `authentication` configuration key in the +PSR-11 container (e.g. [zend-servicemanager](https://github.com/zendframework/zend-servicemanager)). + +The default values are: + +```php +use League\OAuth2\Server\Grant; + +return [ + 'private_key' => __DIR__ . '/../data/oauth/private.key', + 'public_key' => __DIR__ . '/../data/oauth/public.key', + 'encryption_key' => require __DIR__ . '/../data/oauth/encryption.key', + 'access_token_expire' => 'P1D', + 'refresh_token_expire' => 'P1M', + 'auth_code_expire' => 'PT10M', + 'pdo' => [ + 'dsn' => '', + 'username' => '', + 'password' => '' + ], + + // Set value to null to disable a grant + 'grants' => [ + Grant\ClientCredentialsGrant::class => Grant\ClientCredentialsGrant::class, + Grant\PasswordGrant::class => Grant\PasswordGrant::class, + Grant\AuthCodeGrant::class => Grant\AuthCodeGrant::class, + Grant\ImplicitGrant::class => Grant\ImplicitGrant::class, + Grant\RefreshTokenGrant::class => Grant\RefreshTokenGrant::class + ], +]; +``` + +The `private_key` and `public_key` values contains the paths to the previous +generated pair of keys. The `encryption_key` contains the encryption key value +as a string, as stored in the `data/oauth/encryption.key` file. + +By default both key files are checked for correct permissions (chmod 400, 440, +600, 640 or 660 is expected, and 600 or 660 is recommended). In case the +environment/operating system (e.g. Windows) does not support such a permissions, +the check can be disabled: + +```php + // ... + 'private_key' => [ + 'key_or_path' => __DIR__ . '/../data/oauth/private.key', + 'key_permissions_check' => false, + ], + // ... +``` + +The `access_token_expire` value is the time-to-live (TTL) value of the access +token. The time period is represented using the [DateInterval](http://php.net/manual/en/class.dateinterval.php) +format in PHP. The default value is `P1D` (1 day). + +The `refresh_token_expire` value is the TTL used for the refresh token. The +default value is 1 month. + +The `auth_code_expire` value is the TTL of the authentication code, used in +the [authorization code grant](https://oauth2.thephpleague.com/authorization-server/auth-code-grant/) +scenario. The default value is 10 minutes. + +The `pdo` value is for the PDO database configuration. Here we need to insert +the parameters to access the OAuth2 database. These parameters are the `dsn`, +the `username`, and the `password`, if required. The SQL structure of this +database is stored in the [data/oauth2.sql](https://github.com/zendframework/zend-expressive-authentication-oauth2/blob/master/data/oauth2.sql) +file. + +If you already have a PDO service configured, you can instead pass the service +name to the `pdo` key as follows: + +```php +return [ + 'pdo' => 'myServiceName', +]; +``` + +The `grants` array is for enabling/disabling grants. By default, all the supported +grants are configured to be available. If you would like to disable any of the +supplied grants, change the value for the grant to `null`. Additionally, +you can extend this array to add your own custom grants. + +### Configure Event Listeners + +- **Since 1.3.0** + +_Optional_ The `event_listeners` and `event_listener_providers` arrays may be used to enable event listeners for events published by `league\oauth2-server`. See the [Authorization Server Domain Events documentation](https://oauth2.thephpleague.com/authorization-server/events/). The possible event names can be found [in `League\OAuth2\Server\RequestEvent`](https://github.com/thephpleague/oauth2-server/blob/0b0b43d43342c0909b3b32fb7a09d502c368d2ec/src/RequestEvent.php#L17-L22). + +#### Event Listeners + +The `event_listeners` key must contain an array of arrays. Each array element must contain at least 2 elements and may include a 3rd element. These roughly correspond to the arguments passed to [`League\Event\ListenerAcceptorInterface::addListener()`](https://github.com/thephpleague/event/blob/d2cc124cf9a3fab2bb4ff963307f60361ce4d119/src/ListenerAcceptorInterface.php#L43). The first element must be a string -- either the [wildcard (`*`)](https://event.thephpleague.com/2.0/listeners/wildcard/) or a [single event name](https://event.thephpleague.com/2.0/events/named/). The second element must be either a callable, a concrete instance of `League\Event\ListenerInterface`, or a string pointing to your listener service instance in the container. The third element is optional, and must be an integer if provided. + +See the [documentation for callable listeners](https://event.thephpleague.com/2.0/listeners/callables/). + +#### Event Listener Providers + +The `event_listener_providers` key must contain an array. Each array element must contain either a concrete instance of `League\Event\ListenerProviderInterface` or a string pointing to your container service instance of a listener provider. + +See the [documentation for listener providers](https://event.thephpleague.com/2.0/listeners/providers/). + +Example config: + +```php +return [ + 'event_listeners' => [ + // using a container service + [ + \League\OAuth2\Server\RequestEvent::CLIENT_AUTHENTICATION_FAILED, + \My\Event\Listener\Service::class, + ], + // using a callable + [ + \League\OAuth2\Server\RequestEvent::ACCESS_TOKEN_ISSUED, + function (\League\OAuth2\Server\RequestEvent $event) { + // do something + }, + ], + ], + 'event_listener_providers' => [ + \My\Event\ListenerProvider\Service::class, + ], +]; +``` + +## OAuth2 Database + +You need to provide an OAuth2 database yourself, or generate a [SQLite](https://www.sqlite.org) +database with the following command (using `sqlite3` for GNU/Linux): + +```bash +$ sqlite3 data/oauth2.sqlite < vendor/zendframework/zend-expressive-authentication-oauth2/data/oauth2.sql +``` + +You can also create some testing values using the `data/oauth2_test.sql` file: + +```bash +$ sqlite3 data/oauth2.sqlite < vendor/zendframework/zend-expressive-authentication-oauth2/data/oauth2_test.sql +``` + +These commands will insert the following testing values: + +- a client `client_test` with secret `test`, used for [client_credentials](grant/client_credentials.md) + and the [password](grant/password.md) grant type. +- a client `client_test2` with secret `test`, used for [authorization code](grant/auth_code.md) + and [implicit](grant/implicit.md) grant type. +- a user `user_test` with password `test`. +- a `test` scope. + +For security reason, the client `secret` and the user `password` are stored +using the `bcrypt` algorithm as used by the [password_hash](http://php.net/manual/en/function.password-hash.php) +function. + +## Configure OAuth2 Routes + +As the final step, in order to use the OAuth2 server you need to configure the routes +for the **token endpoint** and **authorization**. + +You can read how add the **token endpoint** and the **authorization** routes in +the [Implement an authorization server](authorization-server.md) section. diff --git a/docs/book/v1/usage.md b/docs/book/v1/usage.md new file mode 100644 index 0000000..faed762 --- /dev/null +++ b/docs/book/v1/usage.md @@ -0,0 +1,60 @@ +# Usage + +If you successfully configured the OAuth2 server as detailed in the +[installation](intro.md) section, you can request an access token using the +OAuth2 server route you [defined](intro.md#configure-the-oauth2-route) +(e.g. `/oauth`). + +You can require an access token using one of the following scenarios: + +- [client credentials](grant/client_credentials.md); +- [password](grant/password.md); +- [authorization code](grant/auth_code.md); +- [implicit](grant/implicit.md); +- [refresh token](grant/refresh_token.md). + +## Authenticate a middleware + +This library uses the authentication abstraction of the `Zend\Expressive\Authentication\AuthenticationMiddleware` +class provided by [zend-expressive-authentication](https://github.com/zendframework/zend-expressive-authentication). + +In order to use OAuth2, we need to configure the service +`Zend\Expressive\Authentication\AuthenticationInterface` to resolve to +`Zend\Expressive\Authentication\OAuth2\OAuth2Adapter`. This can be achieved +using the following configuration: + +```php +use Zend\Expressive\Authentication; + +return [ + 'dependencies' => [ + 'aliases' => [ + Authentication\AuthenticationInterface::class => Authentication\OAuth2\OAuth2Adapter::class, + ], + ], +]; +``` + +The previous configuration will instruct `zend-expressive-authentication` to use +the OAuth2 adapter provided in this package. (Unlike other adapters, this +adapter does not require a `Zend\Expressive\Authentication\UserRepositoryInterface`; +the OAuth2 database with user and client credentials is managed by the component +itself.) + +When the service alias is configured, you can immediately begin authenticating +your application/API by adding the `AuthenticationMiddleware` to either your +application or route-specific middleware pipeline. For instance, using an +[Expressive](https://docs.zendframework.com/zend-expressive/) application, you +could add it to a specific route, as follows: + +```php +$app->post('/api/users', [ + Zend\Expressive\Authentication\AuthenticationMiddleware::class, + App\Action\AddUserAction::class, +], 'api.add.user'); +``` + +# Providing an authorization server + +See the chapter [Authorization server](authorization-server.md) for details on how +to implement this. diff --git a/mkdocs.yml b/mkdocs.yml index 105ee8e..4b5b479 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,16 +1,26 @@ docs_dir: docs/book site_dir: docs/html pages: - - index.md - - Introduction: intro.md - - Usage: usage.md + - Home: index.md + - Introduction: v1/intro.md + - Usage: v1/usage.md + - "Authorization server": v1/authorization-server.md + - "Authenticated user": v1/authenticated-user.md - Grant: - - "Client credentials": grant/client_credentials.md - - "Password": grant/password.md - - "Authorization code": grant/auth_code.md - - "Implicit": grant/implicit.md - - "Refresh token": grant/refresh_token.md -site_name: OAuth2 middleware + - "Client credentials": v1/grant/client_credentials.md + - "Password": v1/grant/password.md + - "Authorization code": v1/grant/auth_code.md + - "Implicit": v1/grant/implicit.md + - "Refresh token": v1/grant/refresh_token.md + - "_hidden-legacy-page-links": + - "_introduction": intro.md + - "_usage": usage.md + - "_authorizationserver": authorization-server.md + - "_grant_clientcredentials": grant/client_credentials.md + - "_grant_password": grant/password.md + - "_grant_authcode": grant/auth_code.md + - "_grant_implicit": grant/implicit.md + - "_grant_refreshtoken": grant/refresh_token.md +site_name: zend-expressive-authentication-oauth2 site_description: 'OAuth2 (server) for Expressive and PSR-7 applications.' repo_url: 'https://github.com/zendframework/zend-expressive-authentication-oauth2' -copyright: 'Copyright (c) 2015-2018 Zend Technologies USA Inc.' diff --git a/src/AuthorizationHandler.php b/src/AuthorizationHandler.php new file mode 100644 index 0000000..98478cb --- /dev/null +++ b/src/AuthorizationHandler.php @@ -0,0 +1,54 @@ +server = $server; + $this->responseFactory = function () use ($responseFactory): ResponseInterface { + return $responseFactory(); + }; + } + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $authRequest = $request->getAttribute(AuthorizationRequest::class); + return $this->server->completeAuthorizationRequest($authRequest, ($this->responseFactory)()); + } +} diff --git a/src/AuthorizationHandlerFactory.php b/src/AuthorizationHandlerFactory.php new file mode 100644 index 0000000..ca28a7b --- /dev/null +++ b/src/AuthorizationHandlerFactory.php @@ -0,0 +1,26 @@ +get(AuthorizationServer::class), + $container->get(ResponseInterface::class) + ); + } +} diff --git a/src/AuthorizationMiddleware.php b/src/AuthorizationMiddleware.php new file mode 100644 index 0000000..5a4502f --- /dev/null +++ b/src/AuthorizationMiddleware.php @@ -0,0 +1,79 @@ +server = $server; + $this->responseFactory = function () use ($responseFactory) : ResponseInterface { + return $responseFactory(); + }; + } + + /** + * {@inheritDoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + { + $response = ($this->responseFactory)(); + + try { + $authRequest = $this->server->validateAuthorizationRequest($request); + + // The next handler must take care of providing the + // authenticated user and the approval + $authRequest->setAuthorizationApproved(false); + + return $handler->handle($request->withAttribute(AuthorizationRequest::class, $authRequest)); + } catch (OAuthServerException $exception) { + // The validation throws this exception if the request is not valid + // for example when the client id is invalid + return $exception->generateHttpResponse($response); + } catch (\Exception $exception) { + return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) + ->generateHttpResponse($response); + } + } +} diff --git a/src/OAuth2MiddlewareFactory.php b/src/AuthorizationMiddlewareFactory.php similarity index 53% rename from src/OAuth2MiddlewareFactory.php rename to src/AuthorizationMiddlewareFactory.php index 4a71861..c27d922 100644 --- a/src/OAuth2MiddlewareFactory.php +++ b/src/AuthorizationMiddlewareFactory.php @@ -14,25 +14,12 @@ use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; -use function sprintf; - -class OAuth2MiddlewareFactory +final class AuthorizationMiddlewareFactory { - public function __invoke(ContainerInterface $container) : OAuth2Middleware + public function __invoke(ContainerInterface $container) : AuthorizationMiddleware { - $authServer = $container->has(AuthorizationServer::class) - ? $container->get(AuthorizationServer::class) - : null; - - if (null === $authServer) { - throw new Exception\InvalidConfigException(sprintf( - "The %s service is missing", - AuthorizationServer::class - )); - } - - return new OAuth2Middleware( - $authServer, + return new AuthorizationMiddleware( + $container->get(AuthorizationServer::class), $container->get(ResponseInterface::class) ); } diff --git a/src/AuthorizationServerFactory.php b/src/AuthorizationServerFactory.php index 1ecbc76..b4ad06f 100644 --- a/src/AuthorizationServerFactory.php +++ b/src/AuthorizationServerFactory.php @@ -11,20 +11,30 @@ namespace Zend\Expressive\Authentication\OAuth2; use DateInterval; +use League\Event\ListenerProviderInterface; + use League\OAuth2\Server\AuthorizationServer; -use League\OAuth2\Server\Grant; -use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; -use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; -use League\OAuth2\Server\Repositories\UserRepositoryInterface; use Psr\Container\ContainerInterface; -use Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException; +/** + * Factory for OAuth AuthorizationServer + * + * Initializes a new AuthorizationServer with required params from config. + * Then configured grant types are enabled with configured access token + * expiry. Then any optionally configured event listeners are attached to the + * AuthorizationServer. + */ class AuthorizationServerFactory { use ConfigTrait; use CryptKeyTrait; use RepositoryTrait; + /** + * @param ContainerInterface $container + * + * @return AuthorizationServer + */ public function __invoke(ContainerInterface $container) : AuthorizationServer { $clientRepository = $this->getClientRepository($container); @@ -46,7 +56,7 @@ public function __invoke(ContainerInterface $container) : AuthorizationServer $accessTokenInterval = new DateInterval($this->getAccessTokenExpire($container)); foreach ($grants as $grant) { - // Config may set this grant to null. Continue on if grant has been disabled + // Config may set this grant to null. Continue on if grant has been disabled if (empty($grant)) { continue; } @@ -57,6 +67,76 @@ public function __invoke(ContainerInterface $container) : AuthorizationServer ); } + // add listeners if configured + $this->addListeners($authServer, $container); + + // add listener providers if configured + $this->addListenerProviders($authServer, $container); + return $authServer; } + + /** + * Optionally add event listeners + * + * @param AuthorizationServer $authServer + * @param ContainerInterface $container + */ + private function addListeners( + AuthorizationServer $authServer, + ContainerInterface $container + ): void { + $listeners = $this->getListenersConfig($container); + + foreach ($listeners as $idx => $listenerConfig) { + $event = $listenerConfig[0]; + $listener = $listenerConfig[1]; + $priority = $listenerConfig[2] ?? null; + if (is_string($listener)) { + if (! $container->has($listener)) { + throw new Exception\InvalidConfigException(sprintf( + 'The second element of event_listeners config at ' . + 'index "%s" is a string and therefore expected to ' . + 'be available as a service key in the container. ' . + 'A service named "%s" was not found.', + $idx, + $listener + )); + } + $listener = $container->get($listener); + } + $authServer->getEmitter() + ->addListener($event, $listener, $priority); + } + } + + /** + * Optionally add event listener providers + * + * @param AuthorizationServer $authServer + * @param ContainerInterface $container + */ + private function addListenerProviders( + AuthorizationServer $authServer, + ContainerInterface $container + ): void { + $providers = $this->getListenerProvidersConfig($container); + + foreach ($providers as $idx => $provider) { + if (is_string($provider)) { + if (! $container->has($provider)) { + throw new Exception\InvalidConfigException(sprintf( + 'The event_listener_providers config at ' . + 'index "%s" is a string and therefore expected to ' . + 'be available as a service key in the container. ' . + 'A service named "%s" was not found.', + $idx, + $provider + )); + } + $provider = $container->get($provider); + } + $authServer->getEmitter()->useListenerProvider($provider); + } + } } diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 4a7aaf5..80b0410 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -1,7 +1,7 @@ OAuth2Adapter::class ], 'factories' => [ - OAuth2Middleware::class => OAuth2MiddlewareFactory::class, + AuthorizationMiddleware::class => AuthorizationMiddlewareFactory::class, + AuthorizationHandler::class => AuthorizationHandlerFactory::class, + TokenEndpointHandler::class => TokenEndpointHandlerFactory::class, OAuth2Adapter::class => OAuth2AdapterFactory::class, AuthorizationServer::class => AuthorizationServerFactory::class, ResourceServer::class => ResourceServerFactory::class, @@ -90,7 +95,7 @@ public function getRoutes() : array [ 'name' => 'oauth', 'path' => '/oauth', - 'middleware' => OAuth2Middleware::class, + 'middleware' => AuthorizationMiddleware::class, 'allowed_methods' => ['GET', 'POST'] ], ]; diff --git a/src/ConfigTrait.php b/src/ConfigTrait.php index 0f1c358..1b75670 100644 --- a/src/ConfigTrait.php +++ b/src/ConfigTrait.php @@ -1,7 +1,7 @@ get('config')['authentication'] ?? []; - if (empty($config['grants']) || ! is_array($config['grants'])) { + if (empty($config['grants'])) { throw new InvalidConfigException( - 'The grant value is missing in config authentication' + 'The grants value is missing in config authentication and must be an array' + ); + } + if (! is_array($config['grants'])) { + throw new InvalidConfigException( + 'The grants must be an array value' ); } return $config['grants']; } + + /** + * @param ContainerInterface $container + * + * @return array + */ + protected function getListenersConfig(ContainerInterface $container) : array + { + $config = $container->get('config')['authentication'] ?? []; + + if (empty($config['event_listeners'])) { + return []; + } + if (! is_array($config['event_listeners'])) { + throw new InvalidConfigException( + 'The event_listeners config must be an array value' + ); + } + + return $config['event_listeners']; + } + + /** + * @param ContainerInterface $container + * + * @return array + */ + protected function getListenerProvidersConfig(ContainerInterface $container) : array + { + $config = $container->get('config')['authentication'] ?? []; + + if (empty($config['event_listener_providers'])) { + return []; + } + if (! is_array($config['event_listener_providers'])) { + throw new InvalidConfigException( + 'The event_listener_providers config must be an array value' + ); + } + + return $config['event_listener_providers']; + } } diff --git a/src/Entity/TimestampableTrait.php b/src/Entity/TimestampableTrait.php index 4f710ba..1ae2ccb 100644 --- a/src/Entity/TimestampableTrait.php +++ b/src/Entity/TimestampableTrait.php @@ -1,7 +1,7 @@ createdAt; } - public function setCreatedAt(DateTime $createdAt) : void + public function setCreatedAt(DateTimeInterface $createdAt) : void { $this->createdAt = $createdAt; } - public function getUpdatedAt() : DateTime + public function getUpdatedAt() : DateTimeInterface { return $this->updatedAt; } - public function setUpdatedAt(DateTime $updatedAt) : void + public function setUpdatedAt(DateTimeInterface $updatedAt) : void { $this->updatedAt = $updatedAt; } @@ -54,9 +55,10 @@ public function setUpdatedAt(DateTime $updatedAt) : void public function timestampOnCreate() : void { if (! $this->createdAt) { - $this->createdAt = new DateTime(); if (method_exists($this, 'getTimezone')) { - $this->createdAt->setTimezone(new DateTimeZone($this->getTimezone()->getValue())); + $this->createdAt = new DateTimeImmutable('now', new DateTimeZone($this->getTimezone()->getValue())); + } else { + $this->createdAt = new DateTimeImmutable(); } } } diff --git a/src/OAuth2Adapter.php b/src/OAuth2Adapter.php index f1278f1..c24659c 100644 --- a/src/OAuth2Adapter.php +++ b/src/OAuth2Adapter.php @@ -16,12 +16,9 @@ use Psr\Http\Message\ServerRequestInterface; use Zend\Expressive\Authentication\AuthenticationInterface; use Zend\Expressive\Authentication\UserInterface; -use Zend\Expressive\Authentication\UserRepository\UserTrait; class OAuth2Adapter implements AuthenticationInterface { - use UserTrait; - /** * @var ResourceServer */ @@ -32,12 +29,27 @@ class OAuth2Adapter implements AuthenticationInterface */ protected $responseFactory; - public function __construct(ResourceServer $resourceServer, callable $responseFactory) - { + /** + * @var callable + */ + protected $userFactory; + + public function __construct( + ResourceServer $resourceServer, + callable $responseFactory, + callable $userFactory + ) { $this->resourceServer = $resourceServer; $this->responseFactory = function () use ($responseFactory) : ResponseInterface { return $responseFactory(); }; + $this->userFactory = function ( + string $identity, + array $roles = [], + array $details = [] + ) use ($userFactory) : UserInterface { + return $userFactory($identity, $roles, $details); + }; } /** @@ -47,9 +59,19 @@ public function authenticate(ServerRequestInterface $request) : ?UserInterface { try { $result = $this->resourceServer->validateAuthenticatedRequest($request); - $userId = $result->getAttribute('oauth_user_id', false); - if (false !== $userId) { - return $this->generateUser($userId, []); + $userId = $result->getAttribute('oauth_user_id', null); + $clientId = $result->getAttribute('oauth_client_id', null); + if (isset($userId)) { + return ($this->userFactory)( + $userId, + [], + [ + 'oauth_user_id' => $userId, + 'oauth_client_id' => $clientId, + 'oauth_access_token_id' => $result->getAttribute('oauth_access_token_id', null), + 'oauth_scopes' => $result->getAttribute('oauth_scopes', null) + ] + ); } } catch (OAuthServerException $exception) { return null; @@ -65,7 +87,7 @@ public function unauthorizedResponse(ServerRequestInterface $request) : Response return ($this->responseFactory)() ->withHeader( 'WWW-Authenticate', - 'Bearer token-example' + 'Bearer realm="OAuth2 token"' ) ->withStatus(401); } diff --git a/src/OAuth2AdapterFactory.php b/src/OAuth2AdapterFactory.php index d35e52e..f8b6f91 100644 --- a/src/OAuth2AdapterFactory.php +++ b/src/OAuth2AdapterFactory.php @@ -13,6 +13,7 @@ use League\OAuth2\Server\ResourceServer; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface; +use Zend\Expressive\Authentication\UserInterface; class OAuth2AdapterFactory { @@ -30,7 +31,8 @@ public function __invoke(ContainerInterface $container) : OAuth2Adapter return new OAuth2Adapter( $resourceServer, - $container->get(ResponseInterface::class) + $container->get(ResponseInterface::class), + $container->get(UserInterface::class) ); } } diff --git a/src/OAuth2Middleware.php b/src/OAuth2Middleware.php deleted file mode 100644 index e3d1acd..0000000 --- a/src/OAuth2Middleware.php +++ /dev/null @@ -1,125 +0,0 @@ -server = $server; - $this->responseFactory = function () use ($responseFactory) : ResponseInterface { - return $responseFactory(); - }; - } - - /** - * {@inheritDoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface - { - $method = $request->getMethod(); - switch (strtoupper($method)) { - case 'GET': - return $this->authorizationRequest($request); - case 'POST': - return $this->accessTokenRequest($request); - } - return ($this->responseFactory)()->withStatus(501); // Method not implemented - } - - /** - * Authorize the request and return an authorization code - * Used for authorization code grant and implicit grant - * - * @see https://oauth2.thephpleague.com/authorization-server/auth-code-grant/ - * @see https://oauth2.thephpleague.com/authorization-server/implicit-grant/ - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - protected function authorizationRequest(ServerRequestInterface $request) : ResponseInterface - { - // Create a new response for the request - $response = ($this->responseFactory)(); - - try { - // Validate the HTTP request and return an AuthorizationRequest object. - $authRequest = $this->server->validateAuthorizationRequest($request); - - // The auth request object can be serialized and saved into a user's session. - // You will probably want to redirect the user at this point to a login endpoint. - - // Once the user has logged in set the user on the AuthorizationRequest - $authRequest->setUser(new UserEntity('guest')); // an instance of UserEntityInterface - - // At this point you should redirect the user to an authorization page. - // This form will ask the user to approve the client and the scopes requested. - - // Once the user has approved or denied the client update the status - // (true = approved, false = denied) - $authRequest->setAuthorizationApproved(true); - - // Return the HTTP redirect response - return $this->server->completeAuthorizationRequest($authRequest, $response); - } catch (OAuthServerException $exception) { - return $exception->generateHttpResponse($response); - } catch (\Exception $exception) { - return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) - ->generateHttpResponse($response); - } - } - - /** - * Request an access token - * Used for client credential grant, password grant, and refresh token grant - * - * @see https://oauth2.thephpleague.com/authorization-server/client-credentials-grant/ - * @see https://oauth2.thephpleague.com/authorization-server/resource-owner-password-credentials-grant/ - * @see https://oauth2.thephpleague.com/authorization-server/refresh-token-grant/ - * - * @param ServerRequestInterface $request - * @return ResponseInterface - */ - protected function accessTokenRequest(ServerRequestInterface $request) : ResponseInterface - { - // Create a new response for the request - $response = ($this->responseFactory)(); - - try { - return $this->server->respondToAccessTokenRequest($request, $response); - } catch (OAuthServerException $exception) { - return $exception->generateHttpResponse($response); - } catch (\Exception $exception) { - return (new OAuthServerException($exception->getMessage(), 0, 'unknown_error', 500)) - ->generateHttpResponse($response); - } - } -} diff --git a/src/Repository/Pdo/AbstractRepository.php b/src/Repository/Pdo/AbstractRepository.php index 7cce630..bf688c6 100644 --- a/src/Repository/Pdo/AbstractRepository.php +++ b/src/Repository/Pdo/AbstractRepository.php @@ -23,9 +23,9 @@ class AbstractRepository /** * Constructor * - * @param PdoService $pdo + * @param \PDO $pdo */ - public function __construct(PdoService $pdo) + public function __construct(\PDO $pdo) { $this->pdo = $pdo; } @@ -39,6 +39,10 @@ public function __construct(PdoService $pdo) */ protected function scopesToString(array $scopes) : string { + if (empty($scopes)) { + return ''; + } + return trim(array_reduce($scopes, function ($result, $item) { return $result . ' ' . $item->getIdentifier(); })); diff --git a/src/Repository/Pdo/AccessTokenRepository.php b/src/Repository/Pdo/AccessTokenRepository.php index b1d1f90..03aa739 100644 --- a/src/Repository/Pdo/AccessTokenRepository.php +++ b/src/Repository/Pdo/AccessTokenRepository.php @@ -12,6 +12,7 @@ use League\OAuth2\Server\Entities\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\ClientEntityInterface; +use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\UniqueTokenIdentifierConstraintViolationException; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use Zend\Expressive\Authentication\OAuth2\Entity\AccessTokenEntity; @@ -94,7 +95,7 @@ public function revokeAccessToken($tokenId) $sth = $this->pdo->prepare( 'UPDATE oauth_access_tokens SET revoked=:revoked WHERE id = :tokenId' ); - $sth->bindValue(':revoked', 0); + $sth->bindValue(':revoked', 1); $sth->bindParam(':tokenId', $tokenId); $sth->execute(); @@ -114,6 +115,9 @@ public function isAccessTokenRevoked($tokenId) return false; } $row = $sth->fetch(); + if (! is_array($row)) { + throw OAuthServerException::invalidRefreshToken(); + } return array_key_exists('revoked', $row) ? (bool) $row['revoked'] : false; } diff --git a/src/Repository/Pdo/ClientRepository.php b/src/Repository/Pdo/ClientRepository.php index f6613af..83597a0 100644 --- a/src/Repository/Pdo/ClientRepository.php +++ b/src/Repository/Pdo/ClientRepository.php @@ -1,7 +1,7 @@ pdo->prepare( - 'SELECT * FROM oauth_clients WHERE name = :clientIdentifier' + $clientData = $this->getClientData($clientIdentifier); + + if (empty($clientData)) { + return null; + } + + return new ClientEntity( + $clientIdentifier, + $clientData['name'] ?? '', + $clientData['redirect'] ?? '' ); - $sth->bindParam(':clientIdentifier', $clientIdentifier); + } + + /** + * {@inheritDoc} + */ + public function validateClient($clientIdentifier, $clientSecret, $grantType) : bool + { + $clientData = $this->getClientData($clientIdentifier); - if (false === $sth->execute()) { - return; + if (empty($clientData)) { + return false; } - $row = $sth->fetch(); - if (empty($row) || ! $this->isGranted($row, $grantType)) { - return; + + if (! $this->isGranted($clientData, $grantType)) { + return false; } - if ($mustValidateSecret - && (empty($row['secret']) || ! password_verify((string) $clientSecret, $row['secret'])) - ) { - return; + if (empty($clientData['secret']) || ! password_verify((string) $clientSecret, $clientData['secret'])) { + return false; } - return new ClientEntity($clientIdentifier, $row['name'], $row['redirect']); + return true; } /** * Check the grantType for the client value, stored in $row * - * @param array $row + * @param array $row * @param string $grantType + * * @return bool */ - protected function isGranted(array $row, string $grantType) : bool + protected function isGranted(array $row, string $grantType = null) : bool { switch ($grantType) { case 'authorization_code': @@ -64,4 +79,24 @@ protected function isGranted(array $row, string $grantType) : bool return true; } } + + private function getClientData(string $clientIdentifier) : ?array + { + $statement = $this->pdo->prepare( + 'SELECT * FROM oauth_clients WHERE name = :clientIdentifier' + ); + $statement->bindParam(':clientIdentifier', $clientIdentifier); + + if ($statement->execute() === false) { + return null; + } + + $row = $statement->fetch(); + + if (empty($row)) { + return null; + } + + return $row; + } } diff --git a/src/Repository/Pdo/PdoServiceFactory.php b/src/Repository/Pdo/PdoServiceFactory.php index 4da0057..25e19dc 100644 --- a/src/Repository/Pdo/PdoServiceFactory.php +++ b/src/Repository/Pdo/PdoServiceFactory.php @@ -15,7 +15,7 @@ class PdoServiceFactory { - public function __invoke(ContainerInterface $container) : PdoService + public function __invoke(ContainerInterface $container) : \PDO { $config = $container->has('config') ? $container->get('config') : []; $config = $config['authentication']['pdo'] ?? null; @@ -24,6 +24,17 @@ public function __invoke(ContainerInterface $container) : PdoService 'The PDO configuration is missing' ); } + + if (is_string($config) && ! $container->has($config)) { + throw new Exception\InvalidConfigException( + 'Invalid service for PDO' + ); + } + + if (is_string($config) && $container->has($config)) { + return $container->get($config); + } + if (! isset($config['dsn'])) { throw new Exception\InvalidConfigException( 'The DSN configuration is missing for PDO' diff --git a/src/TokenEndpointHandler.php b/src/TokenEndpointHandler.php new file mode 100644 index 0000000..4c043dc --- /dev/null +++ b/src/TokenEndpointHandler.php @@ -0,0 +1,76 @@ +server = $server; + $this->responseFactory = $responseFactory; + } + + private function createResponse(): ResponseInterface + { + return ($this->responseFactory)(); + } + + /** + * Request an access token + * + * Used for client credential grant, password grant, and refresh token grant + * + * @see https://oauth2.thephpleague.com/authorization-server/client-credentials-grant/ + * @see https://oauth2.thephpleague.com/authorization-server/resource-owner-password-credentials-grant/ + * @see https://oauth2.thephpleague.com/authorization-server/refresh-token-grant/ + * @see https://tools.ietf.org/html/rfc6749#section-3.2 + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $response = $this->createResponse(); + + try { + return $this->server->respondToAccessTokenRequest($request, $response); + } catch (OAuthServerException $exception) { + return $exception->generateHttpResponse($response); + } + } +} diff --git a/src/TokenEndpointHandlerFactory.php b/src/TokenEndpointHandlerFactory.php new file mode 100644 index 0000000..59fee29 --- /dev/null +++ b/src/TokenEndpointHandlerFactory.php @@ -0,0 +1,26 @@ +get(AuthorizationServer::class), + $container->get(ResponseInterface::class) + ); + } +} diff --git a/test/AuthorizationHandlerFactoryTest.php b/test/AuthorizationHandlerFactoryTest.php new file mode 100644 index 0000000..50c5581 --- /dev/null +++ b/test/AuthorizationHandlerFactoryTest.php @@ -0,0 +1,128 @@ +container = $this->prophesize(ContainerInterface::class); + $this->authServer = $this->prophesize(AuthorizationServer::class); + $this->response = $this->prophesize(ResponseInterface::class); + } + + public function testConstructor() + { + $factory = new AuthorizationHandlerFactory(); + $this->assertInstanceOf(AuthorizationHandlerFactory::class, $factory); + } + + public function testRaisesTypeErrorForInvalidAuthorizationServer() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn(new stdClass()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(function () { + }); + + $factory = new AuthorizationHandlerFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); + } + + public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn($this->authServer->reveal()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(new stdClass()); + + $factory = new AuthorizationHandlerFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); + } + + public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseInstance() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn($this->authServer->reveal()); + $this->container + ->get(ResponseInterface::class) + ->will([$this->response, 'reveal']); + + $factory = new AuthorizationHandlerFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); + } + + public function testFactoryReturnsInstanceWhenAppropriateDependenciesArePresentInContainer() + { + $this->container + ->get(AuthorizationServer::class) + ->willReturn($this->authServer->reveal()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(function () { + return $this->response->reveal(); + }); + + $factory = new AuthorizationHandlerFactory(); + $middleware = $factory($this->container->reveal()); + $this->assertInstanceOf(AuthorizationHandler::class, $middleware); + } + + public function testConfigProvider() + { + $authServer = $this->prophesize(AuthorizationServer::class)->reveal(); + $responseFactory = function () { + return $this->prophesize(ResponseInterface::class)->reveal(); + }; + + $container = new ServiceManager((new ConfigProvider())->getDependencies()); + $container->setService(AuthorizationServer::class, $authServer); + $container->setService(ResponseInterface::class, $responseFactory); + + $authHandler = $container->get(AuthorizationHandler::class); + $this->assertInstanceOf(AuthorizationHandler::class, $authHandler); + } +} diff --git a/test/AuthorizationHandlerTest.php b/test/AuthorizationHandlerTest.php new file mode 100644 index 0000000..4e8987a --- /dev/null +++ b/test/AuthorizationHandlerTest.php @@ -0,0 +1,68 @@ +prophesize(AuthorizationServer::class); + $response = $this->prophesize(ResponseInterface::class); + $authRequest = $this->prophesize(AuthorizationRequest::class); + $request = $this->prophesize(ServerRequestInterface::class); + $expectedResponse = $response->reveal(); + + $request->getAttribute(AuthorizationRequest::class) + ->willReturn($authRequest->reveal()); + + $server->completeAuthorizationRequest($authRequest->reveal(), $expectedResponse) + ->shouldBeCalled() + ->willReturn($expectedResponse); + + $subject = new AuthorizationHandler($server->reveal(), function () use ($expectedResponse): ResponseInterface { + return $expectedResponse; + }); + + self::assertSame($expectedResponse, $subject->handle($request->reveal())); + } + + public function testInvalidResponseFactoryThrowsTypeError() + { + $server = $this->prophesize(AuthorizationServer::class); + $authRequest = $this->prophesize(AuthorizationRequest::class); + $request = $this->prophesize(ServerRequestInterface::class); + + $request->getAttribute(AuthorizationRequest::class) + ->willReturn($authRequest->reveal()); + + $server->completeAuthorizationRequest(Argument::any()) + ->shouldNotBeCalled(); + + $subject = new AuthorizationHandler($server->reveal(), function () { + return new stdClass(); + }); + + $this->expectException(TypeError::class); + $subject->handle($request->reveal()); + } +} diff --git a/test/OAuth2MiddlewareFactoryTest.php b/test/AuthorizationMiddlewareFactoryTest.php similarity index 67% rename from test/OAuth2MiddlewareFactoryTest.php rename to test/AuthorizationMiddlewareFactoryTest.php index 021938a..cc7e92b 100644 --- a/test/OAuth2MiddlewareFactoryTest.php +++ b/test/AuthorizationMiddlewareFactoryTest.php @@ -17,25 +17,24 @@ use Psr\Http\Message\ResponseInterface; use stdClass; use TypeError; -use Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException; -use Zend\Expressive\Authentication\OAuth2\OAuth2Middleware; -use Zend\Expressive\Authentication\OAuth2\OAuth2MiddlewareFactory; +use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddleware; +use Zend\Expressive\Authentication\OAuth2\AuthorizationMiddlewareFactory; /** - * @covers \Zend\Expressive\Authentication\OAuth2\OAuth2MiddlewareFactory + * @covers \Zend\Expressive\Authentication\OAuth2\AuthorizationMiddlewareFactory */ -class OAuth2MiddlewareFactoryTest extends TestCase +class AuthorizationMiddlewareFactoryTest extends TestCase { /** @var AuthorizationServer|ObjectProphecy */ private $authServer; - /** @var AuthServer|ObjectProphecy */ + /** @var ContainerInterface|ObjectProphecy */ private $container; /** @var ResponseInterface|ObjectProphecy */ private $response; - public function setUp() + protected function setUp() : void { $this->container = $this->prophesize(ContainerInterface::class); $this->authServer = $this->prophesize(AuthorizationServer::class); @@ -44,23 +43,28 @@ public function setUp() public function testConstructor() { - $factory = new OAuth2MiddlewareFactory(); - $this->assertInstanceOf(OAuth2MiddlewareFactory::class, $factory); + $factory = new AuthorizationMiddlewareFactory(); + $this->assertInstanceOf(AuthorizationMiddlewareFactory::class, $factory); } - public function testInvokeWithEmptyContainer() + public function testRaisesTypeErrorForInvalidAuthorizationServer() { - $factory = new OAuth2MiddlewareFactory(); + $this->container + ->get(AuthorizationServer::class) + ->willReturn(new stdClass()); + $this->container + ->get(ResponseInterface::class) + ->willReturn(function () { + }); - $this->expectException(InvalidConfigException::class); - $middleware = $factory($this->container->reveal()); + $factory = new AuthorizationMiddlewareFactory(); + + $this->expectException(TypeError::class); + $factory($this->container->reveal()); } public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() { - $this->container - ->has(AuthorizationServer::class) - ->willReturn(true); $this->container ->get(AuthorizationServer::class) ->willReturn($this->authServer->reveal()); @@ -68,7 +72,7 @@ public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() ->get(ResponseInterface::class) ->willReturn(new stdClass()); - $factory = new OAuth2MiddlewareFactory(); + $factory = new AuthorizationMiddlewareFactory(); $this->expectException(TypeError::class); $factory($this->container->reveal()); @@ -76,9 +80,6 @@ public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseInstance() { - $this->container - ->has(AuthorizationServer::class) - ->willReturn(true); $this->container ->get(AuthorizationServer::class) ->willReturn($this->authServer->reveal()); @@ -86,7 +87,7 @@ public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseIns ->get(ResponseInterface::class) ->will([$this->response, 'reveal']); - $factory = new OAuth2MiddlewareFactory(); + $factory = new AuthorizationMiddlewareFactory(); $this->expectException(TypeError::class); $factory($this->container->reveal()); @@ -94,9 +95,6 @@ public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseIns public function testFactoryReturnsInstanceWhenAppropriateDependenciesArePresentInContainer() { - $this->container - ->has(AuthorizationServer::class) - ->willReturn(true); $this->container ->get(AuthorizationServer::class) ->willReturn($this->authServer->reveal()); @@ -106,8 +104,8 @@ public function testFactoryReturnsInstanceWhenAppropriateDependenciesArePresentI return $this->response->reveal(); }); - $factory = new OAuth2MiddlewareFactory(); + $factory = new AuthorizationMiddlewareFactory(); $middleware = $factory($this->container->reveal()); - $this->assertInstanceOf(OAuth2Middleware::class, $middleware); + $this->assertInstanceOf(AuthorizationMiddleware::class, $middleware); } } diff --git a/test/AuthorizationMiddlewareTest.php b/test/AuthorizationMiddlewareTest.php new file mode 100644 index 0000000..4b292a4 --- /dev/null +++ b/test/AuthorizationMiddlewareTest.php @@ -0,0 +1,171 @@ +authServer = $this->prophesize(AuthorizationServer::class); + $this->response = $this->prophesize(ResponseInterface::class); + $this->serverRequest = $this->prophesize(ServerRequestInterface::class); + $this->authRequest = $this->prophesize(AuthorizationRequest::class); + $this->handler = $this->prophesize(RequestHandlerInterface::class); + $this->responseFactory = function () { + return $this->response->reveal(); + }; + } + + public function testConstructor() + { + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + + $this->assertInstanceOf(AuthorizationMiddleware::class, $middleware); + $this->assertInstanceOf(MiddlewareInterface::class, $middleware); + } + + public function testProcess() + { + $this->authRequest + ->setUser(Argument::any()) + ->shouldNotBeCalled(); // Ths middleware must not provide a user entity + $this->authRequest + ->setAuthorizationApproved(false) // Expect approval to be set to false only + ->willReturn(null); + + // Mock a valid authorization request + $this->authServer + ->validateAuthorizationRequest($this->serverRequest->reveal()) + ->willReturn($this->authRequest->reveal()); + + // Mock a instance immutability when the authorization request + // is populated + $newRequest = $this->prophesize(ServerRequestInterface::class); + $this->serverRequest + ->withAttribute(AuthorizationRequest::class, $this->authRequest->reveal()) + ->willReturn($newRequest->reveal()); + + // Expect the handler to be called with the new modified request, + // that contains the auth request attribute + $handlerResponse = $this->prophesize(ResponseInterface::class)->reveal(); + $this->handler + ->handle($newRequest->reveal()) + ->willReturn($handlerResponse); + + + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + $response = $middleware->process( + $this->serverRequest->reveal(), + $this->handler->reveal() + ); + + $this->assertSame($handlerResponse, $response); + } + + public function testAuthorizationRequestRaisingOAuthServerExceptionGeneratesResponseFromException() + { + $response = $this->prophesize(ResponseInterface::class); + $oauthServerException = $this->prophesize(OAuthServerException::class); + $oauthServerException + ->generateHttpResponse(Argument::type(ResponseInterface::class)) + ->willReturn($response->reveal()); + + $this->authServer + ->validateAuthorizationRequest($this->serverRequest->reveal()) + ->willThrow($oauthServerException->reveal()); + + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + + $result = $middleware->process( + $this->serverRequest->reveal(), + $this->handler->reveal() + ); + + $this->assertSame($response->reveal(), $result); + } + + public function testAuthorizationRequestRaisingUnknownExceptionGeneratesResponseFromException() + { + $body = $this->prophesize(StreamInterface::class); + $body + ->write(Argument::containingString('oauth2 server error')) + ->shouldBeCalled(); + + $this->response->getBody()->willReturn($body->reveal())->shouldBeCalled(); + $this->response + ->withHeader(Argument::type('string'), Argument::type('string')) + ->willReturn($this->response->reveal()) + ->shouldBeCalled(); + $this->response + ->withStatus(500) + ->willReturn($this->response->reveal()) + ->shouldBeCalled(); + + $exception = new RuntimeException('oauth2 server error'); + + $this->authServer + ->validateAuthorizationRequest($this->serverRequest->reveal()) + ->willThrow($exception); + + $middleware = new AuthorizationMiddleware( + $this->authServer->reveal(), + $this->responseFactory + ); + + $response = $middleware->process( + $this->serverRequest->reveal(), + $this->handler->reveal() + ); + + $this->assertSame($this->response->reveal(), $response); + } +} diff --git a/test/AuthorizationServerFactoryTest.php b/test/AuthorizationServerFactoryTest.php index ef1df13..05d9898 100644 --- a/test/AuthorizationServerFactoryTest.php +++ b/test/AuthorizationServerFactoryTest.php @@ -10,22 +10,32 @@ namespace ZendTest\Expressive\Authentication\OAuth2; +use League\Event\ListenerProviderInterface; + use League\OAuth2\Server\AuthorizationServer; +use League\OAuth2\Server\Grant\PasswordGrant; use League\OAuth2\Server\Grant\ClientCredentialsGrant; use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\ClientRepositoryInterface; use League\OAuth2\Server\Repositories\ScopeRepositoryInterface; use PHPUnit\Framework\TestCase; +use Prophecy\Prophecy\ObjectProphecy; + use Psr\Container\ContainerInterface; use Zend\Expressive\Authentication\OAuth2\AuthorizationServerFactory; +use League\OAuth2\Server\RequestEvent; +use League\Event\ListenerInterface; use function array_merge; use function array_slice; use function in_array; +use Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException; + class AuthorizationServerFactoryTest extends TestCase { + public function testInvoke() { $mockContainer = $this->prophesize(ContainerInterface::class); @@ -41,10 +51,8 @@ public function testInvoke() 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', 'access_token_expire' => 'P1D', 'grants' => [ - ClientCredentialsGrant::class - => ClientCredentialsGrant::class, - PasswordGrant::class - => PasswordGrant::class, + ClientCredentialsGrant::class => ClientCredentialsGrant::class, + PasswordGrant::class => PasswordGrant::class, ], ] ]; @@ -66,4 +74,183 @@ public function testInvoke() $this->assertInstanceOf(AuthorizationServer::class, $result); } + + /** + * @return ObjectProphecy + */ + private function getContainerMock(): ObjectProphecy + { + $mockContainer = $this->prophesize(ContainerInterface::class); + $mockClientRepo = $this->prophesize(ClientRepositoryInterface::class); + $mockAccessTokenRepo = $this->prophesize(AccessTokenRepositoryInterface::class); + $mockScopeRepo = $this->prophesize(ScopeRepositoryInterface::class); + $mockClientGrant = $this->prophesize(GrantTypeInterface::class); + $mockPasswordGrant = $this->prophesize(GrantTypeInterface::class); + + $mockContainer->has(ClientRepositoryInterface::class)->willReturn(true); + $mockContainer->has(AccessTokenRepositoryInterface::class)->willReturn(true); + $mockContainer->has(ScopeRepositoryInterface::class)->willReturn(true); + + $mockContainer->get(ClientRepositoryInterface::class)->willReturn($mockClientRepo->reveal()); + $mockContainer->get(AccessTokenRepositoryInterface::class)->willReturn($mockAccessTokenRepo->reveal()); + $mockContainer->get(ScopeRepositoryInterface::class)->willReturn($mockScopeRepo->reveal()); + $mockContainer->get(ClientCredentialsGrant::class)->willReturn($mockClientGrant->reveal()); + $mockContainer->get(PasswordGrant::class)->willReturn($mockPasswordGrant->reveal()); + + return $mockContainer; + } + + public function testInvokeWithNullGrant() + { + $mockContainer = $this->getContainerMock(); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class => null, + PasswordGrant::class => PasswordGrant::class, + ], + ], + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $result = $factory($mockContainer->reveal()); + + $this->assertInstanceOf(AuthorizationServer::class, $result); + } + + public function testInvokeWithListenerConfig() + { + $mockContainer = $this->getContainerMock(); + $mockListener = $this->prophesize(ListenerInterface::class); + $mockContainer->has(ListenerInterface::class)->willReturn(true); + $mockContainer->get(ListenerInterface::class)->willReturn($mockListener->reveal()); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class => ClientCredentialsGrant::class, + ], + 'event_listeners' => [ + [ + RequestEvent::CLIENT_AUTHENTICATION_FAILED, + function (RequestEvent $event) { + // do something + }, + ], + [ + RequestEvent::CLIENT_AUTHENTICATION_FAILED, + ListenerInterface::class, + ], + ], + ], + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $result = $factory($mockContainer->reveal()); + + $this->assertInstanceOf(AuthorizationServer::class, $result); + } + + public function testInvokeWithListenerConfigMissingServiceThrowsException() + { + $mockContainer = $this->getContainerMock(); + $mockListener = $this->prophesize(ListenerInterface::class); + $mockContainer->has(ListenerInterface::class)->willReturn(false); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class => ClientCredentialsGrant::class, + ], + 'event_listeners' => [ + [ + RequestEvent::CLIENT_AUTHENTICATION_FAILED, + ListenerInterface::class, + ], + ], + ], + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $this->expectException(InvalidConfigException::class); + + $result = $factory($mockContainer->reveal()); + } + + public function testInvokeWithListenerProviderConfig() + { + $mockContainer = $this->getContainerMock(); + $mockProvider = $this->prophesize(ListenerProviderInterface::class); + $mockContainer->has(ListenerProviderInterface::class)->willReturn(true); + $mockContainer->get(ListenerProviderInterface::class)->willReturn($mockProvider->reveal()); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class => ClientCredentialsGrant::class, + ], + 'event_listener_providers' => [ + ListenerProviderInterface::class + ], + ], + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $result = $factory($mockContainer->reveal()); + + $this->assertInstanceOf(AuthorizationServer::class, $result); + } + + public function testInvokeWithListenerProviderConfigMissingServiceThrowsException() + { + $mockContainer = $this->getContainerMock(); + $mockProvider = $this->prophesize(ListenerProviderInterface::class); + $mockContainer->has(ListenerProviderInterface::class)->willReturn(false); + + $config = [ + 'authentication' => [ + 'private_key' => __DIR__ . '/TestAsset/private.key', + 'encryption_key' => 'iALlwJ1sH77dmFCJFo+pMdM6Af4bF/hCca1EDDx7MwE=', + 'access_token_expire' => 'P1D', + 'grants' => [ + ClientCredentialsGrant::class => ClientCredentialsGrant::class, + ], + 'event_listener_providers' => [ + ListenerProviderInterface::class, + ], + ], + ]; + + $mockContainer->get('config')->willReturn($config); + + $factory = new AuthorizationServerFactory(); + + $this->expectException(InvalidConfigException::class); + $factory($mockContainer->reveal()); + } } diff --git a/test/ConfigTraitTest.php b/test/ConfigTraitTest.php new file mode 100644 index 0000000..2808660 --- /dev/null +++ b/test/ConfigTraitTest.php @@ -0,0 +1,233 @@ +trait = $trait = new class { + use ConfigTrait; + + public function proxy(string $name, ContainerInterface $container) + { + return $this->$name($container); + } + }; + $this->config = [ + 'authentication' => [ + 'private_key' => 'xxx', + 'encryption_key' => 'xxx', + 'access_token_expire' => '3600', + 'refresh_token_expire' => '3600', + 'auth_code_expire' => '120', + 'grants' => ['xxx'] + ] + ]; + $this->container = $this->prophesize(ContainerInterface::class); + $this->container + ->get('config') + ->willReturn($this->config); + } + + public function testGetPrivateKeyWhenNoConfigPresentWillResultInAnException() + { + $this->container + ->get('config') + ->willReturn([]); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getPrivateKey', $this->container->reveal()); + } + + public function testGetPrivateKey() + { + $result = $this->trait->proxy('getPrivateKey', $this->container->reveal()); + $this->assertEquals($this->config['authentication']['private_key'], $result); + } + + public function testGetEncryptionKeyNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getEncryptionKey', $this->container->reveal()); + } + + public function testGetEncryptionKey() + { + $result = $this->trait->proxy('getEncryptionKey', $this->container->reveal()); + $this->assertEquals($this->config['authentication']['encryption_key'], $result); + } + + public function testGetAccessTokenExpireNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getAccessTokenExpire', $this->container->reveal()); + } + + public function testGetAccessTokenExpire() + { + $result = $this->trait->proxy('getAccessTokenExpire', $this->container->reveal()); + $this->assertEquals($this->config['authentication']['access_token_expire'], $result); + } + + public function testGetRefreshTokenExpireNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getRefreshTokenExpire', $this->container->reveal()); + } + + public function testGetRefreshTokenExpire() + { + $result = $this->trait->proxy('getRefreshTokenExpire', $this->container->reveal()); + $this->assertEquals($this->config['authentication']['refresh_token_expire'], $result); + } + + public function testGetAuthCodeExpireNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getAuthCodeExpire', $this->container->reveal()); + } + + public function testGetAuthCodeExpire() + { + $result = $this->trait->proxy('getAuthCodeExpire', $this->container->reveal()); + $this->assertEquals($this->config['authentication']['auth_code_expire'], $result); + } + + public function testGetGrantsConfigNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getGrantsConfig', $this->container->reveal()); + } + + public function testGetGrantsConfigNoArrayValue() + { + $this->container + ->get('config') + ->willReturn([ + 'authentication' => [ + 'grants' => 'xxx', + ], + ]); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getGrantsConfig', $this->container->reveal()); + } + + public function testGetGrantsConfig() + { + $result = $this->trait->proxy('getGrantsConfig', $this->container->reveal()); + $this->assertEquals($this->config['authentication']['grants'], $result); + } + + public function testGetListenersConfigNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + $result = $this->trait + ->proxy('getListenersConfig', $this->container->reveal()); + $this->assertInternalType('array', $result); + } + + /** + * @expectedException Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException + */ + public function testGetListenersConfigNoArrayValue() + { + $this->container + ->get('config') + ->willReturn([ + 'authentication' => [ + 'event_listeners' => 'xxx', + ], + ]); + + $this->trait->proxy('getListenersConfig', $this->container->reveal()); + } + + public function testGetListenersConfig() + { + $this->container->get('config') + ->willReturn([ + 'authentication' => [ + 'event_listeners' => $expected = [['xxx']], + ], + ]); + $result = $this->trait + ->proxy('getListenersConfig', $this->container->reveal()); + $this->assertEquals($expected, $result); + } + + public function testGetListenerProvidersConfigNoConfig() + { + $this->container + ->get('config') + ->willReturn([]); + $result = $this->trait + ->proxy('getListenerProvidersConfig', $this->container->reveal()); + $this->assertInternalType('array', $result); + } + + /** + * @expectedException Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException + */ + public function testGetListenerProvidersConfigNoArrayValue() + { + $this->container + ->get('config') + ->willReturn([ + 'authentication' => [ + 'event_listener_providers' => 'xxx', + ], + ]); + + $this->trait->proxy('getListenerProvidersConfig', $this->container->reveal()); + } + + public function testGetListenerProvidersConfig() + { + $this->container->get('config') + ->willReturn([ + 'authentication' => [ + 'event_listener_providers' => $expected = ['xxx'], + ], + ]); + $result = $this->trait + ->proxy('getListenerProvidersConfig', $this->container->reveal()); + $this->assertEquals($expected, $result); + } +} diff --git a/test/Entity/AccessTokenEntityTest.php b/test/Entity/AccessTokenEntityTest.php new file mode 100644 index 0000000..4fad34b --- /dev/null +++ b/test/Entity/AccessTokenEntityTest.php @@ -0,0 +1,24 @@ +assertInstanceOf(AccessTokenEntityInterface::class, $entity); + } +} diff --git a/test/Entity/AuthCodeEntityTest.php b/test/Entity/AuthCodeEntityTest.php new file mode 100644 index 0000000..f7aa8e6 --- /dev/null +++ b/test/Entity/AuthCodeEntityTest.php @@ -0,0 +1,24 @@ +assertInstanceOf(AuthCodeEntityInterface::class, $entity); + } +} diff --git a/test/Entity/ClientEntityTest.php b/test/Entity/ClientEntityTest.php new file mode 100644 index 0000000..210707c --- /dev/null +++ b/test/Entity/ClientEntityTest.php @@ -0,0 +1,67 @@ +entity = new ClientEntity('foo', 'bar', 'http://localhost'); + } + + public function testImplementsAuthCodeEntityInterface() + { + $this->assertInstanceOf(ClientEntityInterface::class, $this->entity); + } + + public function testConstructorSetsIdentifier() + { + $this->assertSame('foo', $this->entity->getIdentifier()); + } + + public function testConstructorSetsName() + { + $this->assertSame('bar', $this->entity->getName()); + } + + public function testConstructorSetsRedirectUri() + { + $this->assertSame(['http://localhost'], $this->entity->getRedirectUri()); + } + + public function testSecret() + { + $this->entity->setSecret('secret'); + $this->assertEquals('secret', $this->entity->getSecret()); + } + + public function testPersonalAccessClient() + { + $this->entity->setPersonalAccessClient(true); + $this->assertTrue($this->entity->hasPersonalAccessClient()); + + $this->entity->setPersonalAccessClient(false); + $this->assertFalse($this->entity->hasPersonalAccessClient()); + } + + public function testPasswordClient() + { + $this->entity->setPasswordClient(true); + $this->assertTrue($this->entity->hasPasswordClient()); + + $this->entity->setPasswordClient(false); + $this->assertFalse($this->entity->hasPasswordClient()); + } +} diff --git a/test/Entity/RefreshTokenEntityTest.php b/test/Entity/RefreshTokenEntityTest.php new file mode 100644 index 0000000..964fb21 --- /dev/null +++ b/test/Entity/RefreshTokenEntityTest.php @@ -0,0 +1,24 @@ +assertInstanceOf(RefreshTokenEntityInterface::class, $entity); + } +} diff --git a/test/Entity/RevokableTraitTest.php b/test/Entity/RevokableTraitTest.php new file mode 100644 index 0000000..b2964b1 --- /dev/null +++ b/test/Entity/RevokableTraitTest.php @@ -0,0 +1,34 @@ +trait = $this->getMockForTrait(RevokableTrait::class); + } + + public function testSetRevokedToTrue() + { + $this->trait->setRevoked(true); + $this->assertTrue($this->trait->isRevoked()); + } + + public function testSetRevokedToFalse() + { + $this->trait->setRevoked(false); + $this->assertFalse($this->trait->isRevoked()); + } +} diff --git a/test/Entity/ScopeEntityTest.php b/test/Entity/ScopeEntityTest.php new file mode 100644 index 0000000..2a70dfb --- /dev/null +++ b/test/Entity/ScopeEntityTest.php @@ -0,0 +1,34 @@ +entity = new ScopeEntity(); + } + + public function testImplementsScopeEntityInterface() + { + $this->assertInstanceOf(ScopeEntityInterface::class, $this->entity); + } + + public function testEntityIsJsonSerializable() + { + $this->entity->setIdentifier('foo'); + $this->assertEquals('"foo"', json_encode($this->entity)); + } +} diff --git a/test/Entity/TimestampableTraitTest.php b/test/Entity/TimestampableTraitTest.php new file mode 100644 index 0000000..3c1c9f7 --- /dev/null +++ b/test/Entity/TimestampableTraitTest.php @@ -0,0 +1,43 @@ +trait = $this->getMockForTrait(TimestampableTrait::class); + } + + public function testCreatedAt() + { + $now = new DateTimeImmutable(); + $this->trait->setCreatedAt($now); + $this->assertEquals($now, $this->trait->getCreatedAt()); + } + + public function testUpdatedAt() + { + $now = new DateTimeImmutable(); + $this->trait->setUpdatedAt($now); + $this->assertEquals($now, $this->trait->getUpdatedAt()); + } + + public function testTimestampOnCreate() + { + $this->trait->timestampOnCreate(); + $this->assertNotEmpty($this->trait->getCreatedAt()); + } +} diff --git a/test/Entity/UserEntityTest.php b/test/Entity/UserEntityTest.php new file mode 100644 index 0000000..c46186a --- /dev/null +++ b/test/Entity/UserEntityTest.php @@ -0,0 +1,43 @@ +entity = new UserEntity('foo'); + } + + public function testConstructorWithoutParamWillResultInAnException() + { + $this->expectException(ArgumentCountError::class); + $entity = new UserEntity(); + } + + public function testImplementsUserEntityInterface() + { + $this->assertInstanceOf(UserEntityInterface::class, $this->entity); + } + + public function testGetIdentifier() + { + $this->assertEquals('foo', $this->entity->getIdentifier()); + } +} diff --git a/test/OAuth2AdapterFactoryTest.php b/test/OAuth2AdapterFactoryTest.php index aa08059..50acbd9 100644 --- a/test/OAuth2AdapterFactoryTest.php +++ b/test/OAuth2AdapterFactoryTest.php @@ -18,8 +18,10 @@ use stdClass; use TypeError; use Zend\Expressive\Authentication\AuthenticationInterface; +use Zend\Expressive\Authentication\OAuth2\Exception; use Zend\Expressive\Authentication\OAuth2\OAuth2Adapter; use Zend\Expressive\Authentication\OAuth2\OAuth2AdapterFactory; +use Zend\Expressive\Authentication\UserInterface; class OAuth2AdapterFactoryTest extends TestCase { @@ -35,7 +37,7 @@ class OAuth2AdapterFactoryTest extends TestCase /** @var callable */ private $responseFactory; - public function setUp() + protected function setUp() : void { $this->container = $this->prophesize(ContainerInterface::class); $this->resourceServer = $this->prophesize(ResourceServer::class); @@ -43,6 +45,14 @@ public function setUp() $this->responseFactory = function () { return $this->response->reveal(); }; + $this->user = $this->prophesize(UserInterface::class); + $this->userFactory = function ( + string $identity, + array $roles = [], + array $details = [] + ) { + return $this->user->reveal($identity, $roles, $details); + }; } public function testConstructor() @@ -51,13 +61,12 @@ public function testConstructor() $this->assertInstanceOf(OAuth2AdapterFactory::class, $factory); } - /** - * @expectedException \Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException - */ public function testInvokeWithEmptyContainer() { $factory = new OAuth2AdapterFactory(); - $oauth2Adapter = $factory($this->container->reveal()); + + $this->expectException(Exception\InvalidConfigException::class); + $factory($this->container->reveal()); } public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() @@ -73,6 +82,10 @@ public function testFactoryRaisesTypeErrorForNonCallableResponseFactory() ->get(ResponseInterface::class) ->willReturn(new stdClass()); + $this->container + ->get(UserInterface::class) + ->willReturn($this->userFactory); + $factory = new OAuth2AdapterFactory(); $this->expectException(TypeError::class); @@ -92,6 +105,10 @@ public function testFactoryRaisesTypeErrorWhenResponseServiceProvidesResponseIns ->get(ResponseInterface::class) ->will([$this->response, 'reveal']); + $this->container + ->get(UserInterface::class) + ->willReturn($this->userFactory); + $factory = new OAuth2AdapterFactory(); $this->expectException(TypeError::class); @@ -114,6 +131,10 @@ public function testFactoryReturnsInstanceWhenAppropriateDependenciesArePresentI ->get(ResponseInterface::class) ->willReturn($this->responseFactory); + $this->container + ->get(UserInterface::class) + ->willReturn($this->userFactory); + $factory = new OAuth2AdapterFactory(); $adapter = $factory($this->container->reveal()); diff --git a/test/OAuth2AdapterTest.php b/test/OAuth2AdapterTest.php index bed3ce8..5bced61 100644 --- a/test/OAuth2AdapterTest.php +++ b/test/OAuth2AdapterTest.php @@ -14,9 +14,11 @@ use League\OAuth2\Server\ResourceServer; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\Prophecy\ObjectProphecy; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Zend\Expressive\Authentication\AuthenticationInterface; +use Zend\Expressive\Authentication\DefaultUser; use Zend\Expressive\Authentication\OAuth2\OAuth2Adapter; use Zend\Expressive\Authentication\UserInterface; @@ -31,20 +33,31 @@ class OAuth2AdapterTest extends TestCase /** @var callable */ private $responseFactory; - public function setUp() + /** @var callable */ + private $userFactory; + + protected function setUp() : void { $this->resourceServer = $this->prophesize(ResourceServer::class); $this->response = $this->prophesize(ResponseInterface::class); $this->responseFactory = function () { return $this->response->reveal(); }; + $this->userFactory = function ( + string $identity, + array $roles = [], + array $details = [] + ) { + return new DefaultUser($identity, $roles, $details); + }; } public function testConstructor() { $adapter = new OAuth2Adapter( $this->resourceServer->reveal(), - $this->responseFactory + $this->responseFactory, + $this->userFactory ); $this->assertInstanceOf(OAuth2Adapter::class, $adapter); $this->assertInstanceOf(AuthenticationInterface::class, $adapter); @@ -62,16 +75,18 @@ public function testOAuthServerExceptionRaisedDuringAuthenticateResultsInInvalid $adapter = new OAuth2Adapter( $this->resourceServer->reveal(), - $this->responseFactory + $this->responseFactory, + $this->userFactory ); $this->assertNull($adapter->authenticate($request->reveal())); } - public function testAuthenticateReturnsNullIfResourceServerDoesNotProduceAUserId() + public function testAuthenticateReturnsNullIfResourceServerDoesNotProduceAUserIdOrClientId() { $request = $this->prophesize(ServerRequestInterface::class); - $request->getAttribute('oauth_user_id', false)->willReturn(false); + $request->getAttribute('oauth_user_id', null)->willReturn(null); + $request->getAttribute('oauth_client_id', null)->willReturn(null); $this->resourceServer ->validateAuthenticatedRequest(Argument::that([$request, 'reveal'])) @@ -79,7 +94,8 @@ public function testAuthenticateReturnsNullIfResourceServerDoesNotProduceAUserId $adapter = new OAuth2Adapter( $this->resourceServer->reveal(), - $this->responseFactory + $this->responseFactory, + $this->userFactory ); $this->assertNull($adapter->authenticate($request->reveal())); @@ -88,7 +104,10 @@ public function testAuthenticateReturnsNullIfResourceServerDoesNotProduceAUserId public function testAuthenticateReturnsAUserIfTheResourceServerProducesAUserId() { $request = $this->prophesize(ServerRequestInterface::class); - $request->getAttribute('oauth_user_id', false)->willReturn('some-identifier'); + $request->getAttribute('oauth_user_id', null)->willReturn('some-identifier'); + $request->getAttribute('oauth_client_id', null)->willReturn(null); + $request->getAttribute('oauth_access_token_id', null)->willReturn(null); + $request->getAttribute('oauth_scopes', null)->willReturn(null); $this->resourceServer ->validateAuthenticatedRequest(Argument::that([$request, 'reveal'])) @@ -96,14 +115,37 @@ public function testAuthenticateReturnsAUserIfTheResourceServerProducesAUserId() $adapter = new OAuth2Adapter( $this->resourceServer->reveal(), - $this->responseFactory + $this->responseFactory, + $this->userFactory ); $user = $adapter->authenticate($request->reveal()); $this->assertInstanceOf(UserInterface::class, $user); $this->assertSame('some-identifier', $user->getIdentity()); - $this->assertSame([], $user->getUserRoles()); + $this->assertSame([], $user->getRoles()); + } + + public function testAuthenticateReturnsNullIfTheResourceServerProducesAClientIdOnly() + { + $request = $this->prophesize(ServerRequestInterface::class); + $request->getAttribute('oauth_user_id', null)->willReturn(null); + $request->getAttribute('oauth_client_id', null)->willReturn('some-identifier'); + $request->getAttribute('oauth_access_token_id', null)->willReturn(null); + $request->getAttribute('oauth_scopes', null)->willReturn(null); + + $this->resourceServer + ->validateAuthenticatedRequest(Argument::that([$request, 'reveal'])) + ->will([$request, 'reveal']); + + $adapter = new OAuth2Adapter( + $this->resourceServer->reveal(), + $this->responseFactory, + $this->userFactory + ); + + $user = $adapter->authenticate($request->reveal()); + $this->assertNull($user); } public function testUnauthorizedResponseProducesAResponseWithAWwwAuthenticateHeader() @@ -111,7 +153,7 @@ public function testUnauthorizedResponseProducesAResponseWithAWwwAuthenticateHea $request = $this->prophesize(ServerRequestInterface::class)->reveal(); $this->response - ->withHeader('WWW-Authenticate', 'Bearer token-example') + ->withHeader('WWW-Authenticate', 'Bearer realm="OAuth2 token"') ->will([$this->response, 'reveal']); $this->response ->withStatus(401) @@ -119,7 +161,8 @@ public function testUnauthorizedResponseProducesAResponseWithAWwwAuthenticateHea $adapter = new OAuth2Adapter( $this->resourceServer->reveal(), - $this->responseFactory + $this->responseFactory, + $this->userFactory ); $this->assertSame( diff --git a/test/OAuth2MiddlewareTest.php b/test/OAuth2MiddlewareTest.php deleted file mode 100644 index b84a213..0000000 --- a/test/OAuth2MiddlewareTest.php +++ /dev/null @@ -1,282 +0,0 @@ -authServer = $this->prophesize(AuthorizationServer::class); - $this->response = $this->prophesize(ResponseInterface::class); - $this->serverRequest = $this->prophesize(ServerRequestInterface::class); - $this->authRequest = $this->prophesize(AuthorizationRequest::class); - $this->handler = $this->prophesize(RequestHandlerInterface::class); - $this->responseFactory = function () { - return $this->response->reveal(); - }; - } - - public function testConstructor() - { - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - $this->assertInstanceOf(OAuth2Middleware::class, $middleware); - $this->assertInstanceOf(MiddlewareInterface::class, $middleware); - } - - public function testProcessWithGet() - { - $this->authRequest - ->setUser(Argument::any()) - ->willReturn(null); - $this->authRequest - ->setAuthorizationApproved(true) - ->willReturn(null); - - $this->serverRequest - ->getMethod() - ->willReturn('GET'); - - $this->authServer - ->completeAuthorizationRequest( - $this->authRequest->reveal(), - $this->response->reveal() - ) - ->willReturn($this->response->reveal()); - $this->authServer - ->validateAuthorizationRequest($this->serverRequest->reveal()) - ->willReturn($this->authRequest); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - $this->assertInstanceOf(ResponseInterface::class, $response); - } - - public function testProcessWithPost() - { - $this->serverRequest->getMethod() - ->willReturn('POST'); - - $this->authServer - ->respondToAccessTokenRequest( - $this->serverRequest->reveal(), - $this->response->reveal() - ) - ->willReturn($this->response->reveal()); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - $this->assertInstanceOf(ResponseInterface::class, $response); - } - - public function testAuthorizationRequestRaisingOAuthServerExceptionGeneratesResponseFromException() - { - $response = $this->prophesize(ResponseInterface::class); - $oauthServerException = $this->prophesize(OAuthServerException::class); - $oauthServerException - ->generateHttpResponse(Argument::type(ResponseInterface::class)) - ->will([$response, 'reveal']); - - $this->authServer - ->validateAuthorizationRequest( - Argument::that([$this->serverRequest, 'reveal']) - ) - ->willThrow($oauthServerException->reveal()); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $this->serverRequest->getMethod()->willReturn('GET'); - - $result = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($response->reveal(), $result); - } - - public function testAuthorizationRequestRaisingUnknownExceptionGeneratesResponseFromException() - { - $body = $this->prophesize(StreamInterface::class); - $body - ->write(Argument::containingString('oauth2 server error')) - ->shouldBeCalled(); - - $this->response->getBody()->will([$body, 'reveal'])->shouldBeCalled(); - $this->response - ->withHeader(Argument::type('string'), Argument::type('string')) - ->will([$this->response, 'reveal']) - ->shouldBeCalled(); - $this->response - ->withStatus(500) - ->will([$this->response, 'reveal']) - ->shouldBeCalled(); - - $exception = new RuntimeException('oauth2 server error'); - - $this->authServer - ->validateAuthorizationRequest( - Argument::that([$this->serverRequest, 'reveal']) - ) - ->willThrow($exception); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $this->serverRequest->getMethod()->willReturn('GET'); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } - - public function testReturns501ResponseForInvalidMethods() - { - $this->serverRequest->getMethod()->willReturn('UNKNOWN'); - $this->response->withStatus(501)->will([$this->response, 'reveal']); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } - - public function testPostRequestResultingInOAuthServerExceptionUsesExceptionToGenerateResponse() - { - $this->serverRequest->getMethod()->willReturn('POST'); - - $exception = $this->prophesize(OAuthServerException::class); - $exception - ->generateHttpResponse(Argument::that([$this->response, 'reveal'])) - ->will([$this->response, 'reveal']); - - $this->authServer - ->respondToAccessTokenRequest( - Argument::that([$this->serverRequest, 'reveal']), - Argument::that([$this->response, 'reveal']) - ) - ->willThrow($exception->reveal()); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } - - public function testPostRequestResultingInGenericExceptionCastsExceptionToOauthServerExceptionToGenerateResponse() - { - $this->serverRequest->getMethod()->willReturn('POST'); - - $exception = new RuntimeException('runtime-exception', 500); - - $body = $this->prophesize(StreamInterface::class); - $body->write(Argument::containingString('runtime-exception'))->shouldBeCalled(); - - $this->response - ->withHeader('Content-type', 'application/json') - ->will([$this->response, 'reveal']); - - $this->response - ->getBody() - ->will([$body, 'reveal']); - - $this->response - ->withStatus(500) - ->will([$this->response, 'reveal']); - - $this->authServer - ->respondToAccessTokenRequest( - Argument::that([$this->serverRequest, 'reveal']), - Argument::that([$this->response, 'reveal']) - ) - ->willThrow($exception); - - $middleware = new OAuth2Middleware( - $this->authServer->reveal(), - $this->responseFactory - ); - - $response = $middleware->process( - $this->serverRequest->reveal(), - $this->handler->reveal() - ); - - $this->assertSame($this->response->reveal(), $response); - } -} diff --git a/test/Pdo/OAuth2PdoMiddlewareTest.php b/test/Pdo/OAuth2PdoMiddlewareTest.php index dfec773..338766d 100644 --- a/test/Pdo/OAuth2PdoMiddlewareTest.php +++ b/test/Pdo/OAuth2PdoMiddlewareTest.php @@ -1,7 +1,7 @@ response = new Response(); $this->pdoService = new PdoService('sqlite:' . self::DB_FILE); @@ -139,15 +153,6 @@ public function setUp() }; } - public function testConstructor() - { - $authMiddleware = new OAuth2Middleware( - $this->authServer, - $this->responseFactory - ); - $this->assertInstanceOf(OAuth2Middleware::class, $authMiddleware); - } - /** * Test the Client Credential Grant * @@ -176,17 +181,17 @@ public function testProcessClientCredentialGrant() [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); $this->assertEquals('Bearer', $content->token_type); - $this->assertInternalType("int", $content->expires_in); + $this->assertIsInt($content->expires_in); $this->assertNotEmpty($content->access_token); } @@ -224,23 +229,23 @@ public function testProcessPasswordGrant() [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); $this->assertEquals('Bearer', $content->token_type); - $this->assertInternalType("int", $content->expires_in); + $this->assertIsInt($content->expires_in); $this->assertNotEmpty($content->access_token); $this->assertNotEmpty($content->refresh_token); } /** - * Test the Authorization Code Grant (Part One) + * Test the Authorization Code Grant flow (Part One) * * @see https://oauth2.thephpleague.com/authorization-server/auth-code-grant/ */ @@ -267,6 +272,17 @@ public function testProcessGetAuthorizationCode() 'scope' => 'test', 'state' => $state ]; + + $codeVerifier = new S256Verifier(); + + $params['code_challenge_method'] = $codeVerifier->getMethod(); + $params['code_verifier'] = self::CODE_VERIFIER; + $params['code_challenge'] = strtr( + rtrim(base64_encode(hash('sha256', self::CODE_VERIFIER, true)), '='), + '+/', + '-_' + ); + $request = $this->buildServerRequest( 'GET', '/auth_code?' . http_build_query($params), @@ -276,12 +292,12 @@ public function testProcessGetAuthorizationCode() $params ); - $authMiddleware = new OAuth2Middleware( - $this->authServer, - $this->responseFactory - ); + // mocks the authorization endpoint pipe + $authMiddleware = new AuthorizationMiddleware($this->authServer, $this->responseFactory); + $authHandler = new AuthorizationHandler($this->authServer, $this->responseFactory); + $consumerHandler = $this->buildConsumerAuthMiddleware($authHandler); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $authMiddleware->process($request, $consumerHandler); $this->assertEquals(302, $response->getStatusCode()); $this->assertTrue($response->hasHeader('Location')); @@ -322,8 +338,10 @@ public function testProcessFromAuthorizationCode(string $code) 'client_id' => 'client_test2', 'client_secret' => 'test', 'redirect_uri' => '/redirect', - 'code' => $code + 'code' => $code, + 'code_verifier' => self::CODE_VERIFIER, ]; + $request = $this->buildServerRequest( 'POST', '/access_token', @@ -332,17 +350,17 @@ public function testProcessFromAuthorizationCode(string $code) [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); $this->assertEquals('Bearer', $content->token_type); - $this->assertInternalType("int", $content->expires_in); + $this->assertIsInt($content->expires_in); $this->assertNotEmpty($content->access_token); $this->assertNotEmpty($content->refresh_token); @@ -379,12 +397,11 @@ public function testProcessImplicitGrant() $params ); - $authMiddleware = new OAuth2Middleware( - $this->authServer, - $this->responseFactory - ); + $authMiddleware = new AuthorizationMiddleware($this->authServer, $this->responseFactory); + $authHandler = new AuthorizationHandler($this->authServer, $this->responseFactory); + $consumerHandler = $this->buildConsumerAuthMiddleware($authHandler); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $authMiddleware->process($request, $consumerHandler); $this->assertEquals(302, $response->getStatusCode()); $this->assertTrue($response->hasHeader('Location')); @@ -432,21 +449,50 @@ public function testProcessRefreshTokenGrant(string $refreshToken) [ 'Content-Type' => 'application/x-www-form-urlencoded' ] ); - $authMiddleware = new OAuth2Middleware( + $handler = new TokenEndpointHandler( $this->authServer, $this->responseFactory ); - $response = $authMiddleware->process($request, $this->handler->reveal()); + $response = $handler->handle($request); $this->assertEquals(200, $response->getStatusCode()); $content = json_decode((string) $response->getBody()); $this->assertEquals('Bearer', $content->token_type); - $this->assertInternalType("int", $content->expires_in); + $this->assertIsInt($content->expires_in); $this->assertNotEmpty($content->access_token); $this->assertNotEmpty($content->refresh_token); } + private function buildConsumerAuthMiddleware(AuthorizationHandler $authHandler) + { + return new class($authHandler) implements RequestHandlerInterface + { + /** + * @var AuthorizationHandler + */ + private $handler; + + public function __construct(AuthorizationHandler $handler) + { + $this->handler = $handler; + } + + public function handle( + ServerRequestInterface $request + ): ResponseInterface { + $authRequest = $request->getAttribute(AuthorizationRequest::class); + assert($authRequest instanceof AuthorizationRequest); + $authRequest->setUser(new UserEntity('test')); + $authRequest->setAuthorizationApproved(true); + + return $this->handler->handle( + $request->withAttribute(AuthorizationRequest::class, $authRequest) + ); + } + }; + } + /** * Build a ServerRequest object */ diff --git a/test/Repository/Pdo/AbstractRepositoryTest.php b/test/Repository/Pdo/AbstractRepositoryTest.php new file mode 100644 index 0000000..de275d1 --- /dev/null +++ b/test/Repository/Pdo/AbstractRepositoryTest.php @@ -0,0 +1,41 @@ +pdo = $this->prophesize(PdoService::class); + } + + public function testConstructor() + { + $abstract = new AbstractRepository($this->pdo->reveal()); + $this->assertInstanceOf(AbstractRepository::class, $abstract); + } + + public function testScopesToStringWithEmptyArray() + { + $proxy = new class($this->pdo->reveal()) extends AbstractRepository { + public function scopesToString(array $scopes): string + { + return parent::scopesToString($scopes); + } + }; + $result = $proxy->scopesToString([]); + $this->assertEquals('', $result); + } +} diff --git a/test/Repository/Pdo/AccessTokenRepositoryFactoryTest.php b/test/Repository/Pdo/AccessTokenRepositoryFactoryTest.php new file mode 100644 index 0000000..27807b0 --- /dev/null +++ b/test/Repository/Pdo/AccessTokenRepositoryFactoryTest.php @@ -0,0 +1,41 @@ +container = $this->prophesize(ContainerInterface::class); + $this->pdo = $this->prophesize(PdoService::class); + } + + public function testFactory() + { + $this->container + ->get(PdoService::class) + ->willReturn($this->pdo->reveal()); + + $factory = (new AccessTokenRepositoryFactory)($this->container->reveal()); + $this->assertInstanceOf(AccessTokenRepository::class, $factory); + } +} diff --git a/test/Repository/Pdo/AccessTokenRepositoryTest.php b/test/Repository/Pdo/AccessTokenRepositoryTest.php index fc7c083..85b61b9 100644 --- a/test/Repository/Pdo/AccessTokenRepositoryTest.php +++ b/test/Repository/Pdo/AccessTokenRepositoryTest.php @@ -1,7 +1,7 @@ pdo = $this->prophesize(PdoService::class); $this->repo = new AccessTokenRepository($this->pdo->reveal()); @@ -125,4 +133,55 @@ public function testIsAccessTokenRevokedReturnsTrueWhenRowRevokedFlagIsTrue() $this->assertTrue($this->repo->isAccessTokenRevoked('token_id')); } + + public function testIsAcessTokenRevokedRaisesExceptionWhenTokenIdDontExists() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':tokenId', 'token_id')->shouldBeCalled(); + $statement->execute()->willReturn(true)->shouldBeCalled(); + $statement->fetch()->willReturn(false)->shouldBeCalled(); + + $this->pdo + ->prepare(Argument::containingString('SELECT revoked FROM oauth_access_tokens')) + ->will([$statement, 'reveal']); + + $this->expectException(OAuthServerException::class); + $this->repo->isAccessTokenRevoked('token_id'); + } + + public function testRevokeAccessToken() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':tokenId', 'token_id')->shouldBeCalled(); + $statement->bindValue(':revoked', 1)->shouldBeCalled(); + $statement->execute()->willReturn(null)->shouldBeCalled(); + + $this->pdo + ->prepare(Argument::containingString('UPDATE oauth_access_tokens SET revoked=:revoked')) + ->will([$statement, 'reveal']); + + $this->repo->revokeAccessToken('token_id'); + } + + public function testGetNewToken() + { + $client = $this->prophesize(ClientEntityInterface::class)->reveal(); + $accessToken = $this->repo->getNewToken($client, []); + $this->assertInstanceOf(AccessTokenEntity::class, $accessToken); + $this->assertEquals($client, $accessToken->getClient()); + $this->assertEquals([], $accessToken->getScopes()); + } + + public function testGetNewTokenWithScopeAndIndentifier() + { + $client = $this->prophesize(ClientEntityInterface::class)->reveal(); + $scopes = [ $this->prophesize(ScopeEntityInterface::class)->reveal() ]; + $userIdentifier = 'foo'; + + $accessToken = $this->repo->getNewToken($client, $scopes, $userIdentifier); + $this->assertInstanceOf(AccessTokenEntity::class, $accessToken); + $this->assertEquals($client, $accessToken->getClient()); + $this->assertEquals($scopes, $accessToken->getScopes()); + $this->assertEquals($userIdentifier, $accessToken->getUserIdentifier()); + } } diff --git a/test/Repository/Pdo/AuthCodeRepositoryFactoryTest.php b/test/Repository/Pdo/AuthCodeRepositoryFactoryTest.php new file mode 100644 index 0000000..2897242 --- /dev/null +++ b/test/Repository/Pdo/AuthCodeRepositoryFactoryTest.php @@ -0,0 +1,41 @@ +container = $this->prophesize(ContainerInterface::class); + $this->pdo = $this->prophesize(PdoService::class); + } + + public function testFactory() + { + $this->container + ->get(PdoService::class) + ->willReturn($this->pdo->reveal()); + + $factory = (new AuthCodeRepositoryFactory)($this->container->reveal()); + $this->assertInstanceOf(AuthCodeRepository::class, $factory); + } +} diff --git a/test/Repository/Pdo/AuthCodeRepositoryTest.php b/test/Repository/Pdo/AuthCodeRepositoryTest.php index d960fb0..64e297a 100644 --- a/test/Repository/Pdo/AuthCodeRepositoryTest.php +++ b/test/Repository/Pdo/AuthCodeRepositoryTest.php @@ -1,7 +1,7 @@ pdo = $this->prophesize(PdoService::class); $this->repo = new AuthCodeRepository($this->pdo->reveal()); } - public function testPeristNewAuthCodeRaisesExceptionWhenStatementExecutionFails() + public function testPersistNewAuthCodeRaisesExceptionWhenStatementExecutionFails() { $client = $this->prophesize(ClientEntityInterface::class); $client->getIdentifier()->willReturn('client_id'); @@ -81,4 +81,38 @@ public function testIsAuthCodeRevokedReturnsFalseForStatementExecutionFailure() $this->assertFalse($this->repo->isAuthCodeRevoked('code_identifier')); } + + public function testIsAuthCodeRevokedReturnsTrue() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':codeId', 'code_identifier')->shouldBeCalled(); + $statement->execute()->willReturn(true); + $statement->fetch()->willReturn(['revoked' => true]); + + $this->pdo + ->prepare(Argument::containingString('SELECT revoked FROM oauth_auth_codes')) + ->will([$statement, 'reveal']); + + $this->assertTrue($this->repo->isAuthCodeRevoked('code_identifier')); + } + + public function testNewAuthCode() + { + $result = $this->repo->getNewAuthCode(); + $this->assertInstanceOf(AuthCodeEntity::class, $result); + } + + public function testRevokeAuthCode() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':codeId', 'code_identifier')->shouldBeCalled(); + $statement->bindValue(':revoked', 1)->shouldBeCalled(); + $statement->execute()->willReturn(true); + + $this->pdo + ->prepare(Argument::containingString('UPDATE oauth_auth_codes SET revoked=:revoked WHERE id = :codeId')) + ->will([$statement, 'reveal']); + + $this->repo->revokeAuthCode('code_identifier'); + } } diff --git a/test/Repository/Pdo/ClientRepositoryFactoryTest.php b/test/Repository/Pdo/ClientRepositoryFactoryTest.php new file mode 100644 index 0000000..e1a3072 --- /dev/null +++ b/test/Repository/Pdo/ClientRepositoryFactoryTest.php @@ -0,0 +1,41 @@ +container = $this->prophesize(ContainerInterface::class); + $this->pdo = $this->prophesize(PdoService::class); + } + + public function testFactory() + { + $this->container + ->get(PdoService::class) + ->willReturn($this->pdo->reveal()); + + $factory = (new ClientRepositoryFactory)($this->container->reveal()); + $this->assertInstanceOf(ClientRepository::class, $factory); + } +} diff --git a/test/Repository/Pdo/ClientRepositoryTest.php b/test/Repository/Pdo/ClientRepositoryTest.php index 312601e..6ae6636 100644 --- a/test/Repository/Pdo/ClientRepositoryTest.php +++ b/test/Repository/Pdo/ClientRepositoryTest.php @@ -1,7 +1,7 @@ pdo = $this->prophesize(PdoService::class); $this->repo = new ClientRepository($this->pdo->reveal()); @@ -36,10 +36,7 @@ public function testGetClientEntityReturnsNullIfStatementExecutionReturnsFalse() ->will([$statement, 'reveal']); $this->assertNull( - $this->repo ->getClientEntity( - 'client_id', - 'grant_type' - ) + $this->repo ->getClientEntity('client_id') ); } @@ -59,10 +56,45 @@ public function testGetClientEntityReturnsNullIfNoRowReturned() $client = $this->prophesize(ClientEntityInterface::class); $this->assertNull( - $this->repo ->getClientEntity( - 'client_id', - 'grant_type' - ) + $this->repo ->getClientEntity('client_id') + ); + } + + public function testGetClientEntityReturnsCorrectEntity() + { + $name = 'foo'; + $redirect = 'bar'; + + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':clientIdentifier', 'client_id')->shouldBeCalled(); + $statement->execute()->will(function () use ($statement, $name, $redirect) { + $statement->fetch()->willReturn([ + 'name' => $name, + 'redirect' => $redirect, + ]); + return null; + }); + + $this->pdo + ->prepare(Argument::containingString('SELECT * FROM oauth_clients')) + ->will([$statement, 'reveal']); + + $this->prophesize(ClientEntityInterface::class); + + /** @var ClientEntityInterface $client */ + $client = $this->repo->getClientEntity('client_id'); + + $this->assertInstanceOf( + ClientEntityInterface::class, + $client + ); + $this->assertEquals( + $name, + $client->getName() + ); + $this->assertEquals( + [$redirect], + $client->getRedirectUri() ); } @@ -82,10 +114,34 @@ public function invalidGrants() ]; } + public function testValidateClientReturnsFalseIfNoRowReturned() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':clientIdentifier', 'client_id')->shouldBeCalled(); + $statement->execute()->will(function () use ($statement) { + $statement->fetch()->willReturn([]); + return null; + }); + + $this->pdo + ->prepare(Argument::containingString('SELECT * FROM oauth_clients')) + ->will([$statement, 'reveal']); + + $client = $this->prophesize(ClientEntityInterface::class); + + $this->assertFalse( + $this->repo->validateClient( + 'client_id', + '', + 'password' + ) + ); + } + /** * @dataProvider invalidGrants */ - public function testGetClientEntityReturnsNullIfRowIndicatesNotGranted(string $grantType, array $rowReturned) + public function testValidateClientReturnsFalseIfRowIndicatesNotGranted(string $grantType, array $rowReturned) { $statement = $this->prophesize(PDOStatement::class); $statement->bindParam(':clientIdentifier', 'client_id')->shouldBeCalled(); @@ -100,22 +156,23 @@ public function testGetClientEntityReturnsNullIfRowIndicatesNotGranted(string $g $client = $this->prophesize(ClientEntityInterface::class); - $this->assertNull( - $this->repo ->getClientEntity( + $this->assertFalse( + $this->repo ->validateClient( 'client_id', + '', $grantType ) ); } - public function testGetClientReturnsNullForNonMatchingClientSecret() + public function testValidateClientReturnsFalseForNonMatchingClientSecret() { $statement = $this->prophesize(PDOStatement::class); $statement->bindParam(':clientIdentifier', 'client_id')->shouldBeCalled(); $statement->execute()->will(function () use ($statement) { $statement->fetch()->willReturn([ 'password_client' => true, - 'secret' => 'unknown password', + 'secret' => 'bar', ]); return null; }); @@ -126,17 +183,16 @@ public function testGetClientReturnsNullForNonMatchingClientSecret() $client = $this->prophesize(ClientEntityInterface::class); - $this->assertNull( - $this->repo ->getClientEntity( + $this->assertFalse( + $this->repo ->validateClient( 'client_id', - 'password_client', - 'password', - true + 'foo', + 'password' ) ); } - public function testGetClientReturnsNullForEmptyClientSecret() + public function testValidateClientReturnsFalseForEmptyClientSecret() { $statement = $this->prophesize(PDOStatement::class); $statement->bindParam(':clientIdentifier', 'client_id')->shouldBeCalled(); @@ -154,12 +210,11 @@ public function testGetClientReturnsNullForEmptyClientSecret() $client = $this->prophesize(ClientEntityInterface::class); - $this->assertNull( - $this->repo ->getClientEntity( + $this->assertFalse( + $this->repo ->validateClient( 'client_id', - 'password_client', - 'password', - true + 'foo', + 'password' ) ); } diff --git a/test/Repository/Pdo/PdoServiceFactoryTest.php b/test/Repository/Pdo/PdoServiceFactoryTest.php index 6a37479..cc7c9fe 100644 --- a/test/Repository/Pdo/PdoServiceFactoryTest.php +++ b/test/Repository/Pdo/PdoServiceFactoryTest.php @@ -18,7 +18,7 @@ class PdoServiceFactoryTest extends TestCase { - public function setUp() + protected function setUp() : void { $this->container = $this->prophesize(ContainerInterface::class); $this->factory = new PdoServiceFactory(); @@ -73,4 +73,39 @@ public function testValidConfigurationResultsInReturnedPdoServiceInstance() $this->assertInstanceOf(PdoService::class, $pdo); } + + public function testValidServiceInConfigurationReturnsPdoService() + { + $mockPdo = $this->prophesize(\PDO::class); + + $this->container->has('config')->willReturn(true); + $this->container->get('config')->willReturn([ + 'authentication' => [ + 'pdo' => 'My\Pdo\Service', + ], + ]); + + $this->container->has('My\Pdo\Service')->willReturn(true); + $this->container->get('My\Pdo\Service')->willReturn($mockPdo->reveal()); + + $pdo = ($this->factory)($this->container->reveal()); + + $this->assertInstanceOf(\PDO::class, $pdo); + } + + public function testRaisesExceptionIfPdoServiceIsInvalid() + { + $this->container->has('config')->willReturn(true); + $this->container->get('config')->willReturn([ + 'authentication' => [ + 'pdo' => 'My\Invalid\Service', + ], + ]); + + $this->container->has('My\Invalid\Service')->willReturn(false); + + $this->expectException(Exception\InvalidConfigException::class); + + ($this->factory)($this->container->reveal()); + } } diff --git a/test/Repository/Pdo/RefreshTokenRepositoryFactoryTest.php b/test/Repository/Pdo/RefreshTokenRepositoryFactoryTest.php new file mode 100644 index 0000000..7fd6a0b --- /dev/null +++ b/test/Repository/Pdo/RefreshTokenRepositoryFactoryTest.php @@ -0,0 +1,41 @@ +container = $this->prophesize(ContainerInterface::class); + $this->pdo = $this->prophesize(PdoService::class); + } + + public function testFactory() + { + $this->container + ->get(PdoService::class) + ->willReturn($this->pdo->reveal()); + + $factory = (new RefreshTokenRepositoryFactory)($this->container->reveal()); + $this->assertInstanceOf(RefreshTokenRepository::class, $factory); + } +} diff --git a/test/Repository/Pdo/RefreshTokenRepositoryTest.php b/test/Repository/Pdo/RefreshTokenRepositoryTest.php index f00ccf6..c726839 100644 --- a/test/Repository/Pdo/RefreshTokenRepositoryTest.php +++ b/test/Repository/Pdo/RefreshTokenRepositoryTest.php @@ -1,7 +1,7 @@ pdo = $this->prophesize(PdoService::class); $this->repo = new RefreshTokenRepository($this->pdo->reveal()); @@ -73,4 +74,41 @@ public function testIsRefreshTokenRevokedReturnsFalseWhenStatementFailsExecution $this->assertFalse($this->repo->isRefreshTokenRevoked('token_id')); } + + public function testIsRefreshTokenRevokedReturnsTrue() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':tokenId', 'token_id')->shouldBeCalled(); + $statement->execute()->willReturn(true)->shouldBeCalled(); + $statement->fetch()->willReturn(['revoked' => true]); + + $this->pdo + ->prepare(Argument::containingString('SELECT revoked FROM oauth_refresh_tokens')) + ->will([$statement, 'reveal']); + + $this->assertTrue($this->repo->isRefreshTokenRevoked('token_id')); + } + + public function testGetNewRefreshToken() + { + $result = $this->repo->getNewRefreshToken(); + $this->assertInstanceOf(RefreshTokenEntity::class, $result); + } + + public function testRevokeRefreshToken() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':tokenId', 'token_id')->shouldBeCalled(); + $statement->bindValue(':revoked', 1)->shouldBeCalled(); + $statement->execute()->willReturn(true)->shouldBeCalled(); + $statement->fetch()->shouldNotBeCalled(); + + $this->pdo + ->prepare(Argument::containingString( + 'UPDATE oauth_refresh_tokens SET revoked=:revoked WHERE id = :tokenId' + )) + ->will([$statement, 'reveal']); + + $this->repo->revokeRefreshToken('token_id'); + } } diff --git a/test/Repository/Pdo/ScopeRepositoryFactoryTest.php b/test/Repository/Pdo/ScopeRepositoryFactoryTest.php new file mode 100644 index 0000000..55c0da3 --- /dev/null +++ b/test/Repository/Pdo/ScopeRepositoryFactoryTest.php @@ -0,0 +1,41 @@ +container = $this->prophesize(ContainerInterface::class); + $this->pdo = $this->prophesize(PdoService::class); + } + + public function testFactory() + { + $this->container + ->get(PdoService::class) + ->willReturn($this->pdo->reveal()); + + $factory = (new ScopeRepositoryFactory)($this->container->reveal()); + $this->assertInstanceOf(ScopeRepository::class, $factory); + } +} diff --git a/test/Repository/Pdo/ScopeRepositoryTest.php b/test/Repository/Pdo/ScopeRepositoryTest.php index fce2cc1..e30f88b 100644 --- a/test/Repository/Pdo/ScopeRepositoryTest.php +++ b/test/Repository/Pdo/ScopeRepositoryTest.php @@ -1,7 +1,7 @@ pdo = $this->prophesize(PdoService::class); $this->repo = new ScopeRepository($this->pdo->reveal()); @@ -51,4 +53,29 @@ public function testGetScopeEntityByIdentifierReturnsNullWhenReturnedRowDoesNotH $this->assertNull($this->repo->getScopeEntityByIdentifier('id')); } + + public function testGetScopeEntityByIndentifierReturnsScopes() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':identifier', 'id')->shouldBeCalled(); + $statement->execute()->shouldBeCalled(); + $statement->fetch()->willReturn([ + 'id' => 'foo' + ])->shouldBeCalled(); + + $this->pdo + ->prepare(Argument::containingString('SELECT id FROM oauth_scopes')) + ->will([$statement, 'reveal']); + + $scope = $this->repo->getScopeEntityByIdentifier('id'); + $this->assertInstanceOf(ScopeEntity::class, $scope); + $this->assertEquals('foo', $scope->getIdentifier()); + } + + public function testFinalizeScopesWithEmptyScopes() + { + $clientEntity = $this->prophesize(ClientEntityInterface::class); + $scopes = $this->repo->finalizeScopes([], 'foo', $clientEntity->reveal()); + $this->assertEquals([], $scopes); + } } diff --git a/test/Repository/Pdo/UserRepositoryTest.php b/test/Repository/Pdo/UserRepositoryTest.php index 6cd1e4e..7eac734 100644 --- a/test/Repository/Pdo/UserRepositoryTest.php +++ b/test/Repository/Pdo/UserRepositoryTest.php @@ -1,7 +1,7 @@ pdo = $this->prophesize(PdoService::class); $this->repo = new UserRepository($this->pdo->reveal()); @@ -98,4 +99,29 @@ public function testGetUserEntityByCredentialsReturnsNullIfUserIsNotFound() ) ); } + + public function testGetUserEntityByCredentialsReturnsEntity() + { + $statement = $this->prophesize(PDOStatement::class); + $statement->bindParam(':username', 'username')->shouldBeCalled(); + $statement->execute()->willReturn(true); + $statement->fetch()->willReturn([ + 'password' => password_hash('password', PASSWORD_DEFAULT) + ]); + + $this->pdo + ->prepare(Argument::containingString('SELECT password FROM oauth_users WHERE username = :username')) + ->will([$statement, 'reveal']); + + $client = $this->prophesize(ClientEntityInterface::class); + + $entity = $this->repo->getUserEntityByUserCredentials( + 'username', + 'password', + 'auth', + $client->reveal() + ); + $this->assertInstanceOf(UserEntity::class, $entity); + $this->assertEquals('username', $entity->getIdentifier()); + } } diff --git a/test/RepositoryTraitTest.php b/test/RepositoryTraitTest.php new file mode 100644 index 0000000..5facc85 --- /dev/null +++ b/test/RepositoryTraitTest.php @@ -0,0 +1,176 @@ +trait = new class { + use RepositoryTrait; + + public function proxy(string $name, ContainerInterface $container) + { + return $this->$name($container); + } + }; + $this->container = $this->prophesize(ContainerInterface::class); + } + + public function testGetUserRepositoryWithoutService() + { + $this->container + ->has(UserRepositoryInterface::class) + ->willReturn(false); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getUserRepository', $this->container->reveal()); + } + + public function testGetUserRepository() + { + $this->container + ->has(UserRepositoryInterface::class) + ->willReturn(true); + $this->container + ->get(UserRepositoryInterface::class) + ->willReturn($this->prophesize(UserRepositoryInterface::class)->reveal()); + + $result = $this->trait->proxy('getUserRepository', $this->container->reveal()); + $this->assertInstanceOf(UserRepositoryInterface::class, $result); + } + + public function testGetScopeRepositoryWithoutService() + { + $this->container + ->has(ScopeRepositoryInterface::class) + ->willReturn(false); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getScopeRepository', $this->container->reveal()); + } + + public function testGetScopeRepository() + { + $this->container + ->has(ScopeRepositoryInterface::class) + ->willReturn(true); + $this->container + ->get(ScopeRepositoryInterface::class) + ->willReturn($this->prophesize(ScopeRepositoryInterface::class)->reveal()); + + $result = $this->trait->proxy('getScopeRepository', $this->container->reveal()); + $this->assertInstanceOf(ScopeRepositoryInterface::class, $result); + } + + public function testGetAccessTokenRepositoryWithoutService() + { + $this->container + ->has(AccessTokenRepositoryInterface::class) + ->willReturn(false); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getAccessTokenRepository', $this->container->reveal()); + } + + public function testGetAccessTokenRepository() + { + $this->container + ->has(AccessTokenRepositoryInterface::class) + ->willReturn(true); + $this->container + ->get(AccessTokenRepositoryInterface::class) + ->willReturn($this->prophesize(AccessTokenRepositoryInterface::class)->reveal()); + + $result = $this->trait->proxy('getAccessTokenRepository', $this->container->reveal()); + $this->assertInstanceOf(AccessTokenRepositoryInterface::class, $result); + } + + public function testGetClientRepositoryWithoutService() + { + $this->container + ->has(ClientRepositoryInterface::class) + ->willReturn(false); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getClientRepository', $this->container->reveal()); + } + + public function testGetClientRepository() + { + $this->container + ->has(ClientRepositoryInterface::class) + ->willReturn(true); + $this->container + ->get(ClientRepositoryInterface::class) + ->willReturn($this->prophesize(ClientRepositoryInterface::class)->reveal()); + + $result = $this->trait->proxy('getClientRepository', $this->container->reveal()); + $this->assertInstanceOf(ClientRepositoryInterface::class, $result); + } + + public function testGetRefreshTokenRepositoryWithoutService() + { + $this->container + ->has(RefreshTokenRepositoryInterface::class) + ->willReturn(false); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getRefreshTokenRepository', $this->container->reveal()); + } + + public function testGetRefreshTokenRepository() + { + $this->container + ->has(RefreshTokenRepositoryInterface::class) + ->willReturn(true); + $this->container + ->get(RefreshTokenRepositoryInterface::class) + ->willReturn($this->prophesize(RefreshTokenRepositoryInterface::class)->reveal()); + + $result = $this->trait->proxy('getRefreshTokenRepository', $this->container->reveal()); + $this->assertInstanceOf(RefreshTokenRepositoryInterface::class, $result); + } + + public function testGetAuthCodeRepositoryWithoutService() + { + $this->container + ->has(AuthCodeRepositoryInterface::class) + ->willReturn(false); + + $this->expectException(Exception\InvalidConfigException::class); + $this->trait->proxy('getAuthCodeRepository', $this->container->reveal()); + } + + public function testGetAuthCodeRepository() + { + $this->container + ->has(AuthCodeRepositoryInterface::class) + ->willReturn(true); + $this->container + ->get(AuthCodeRepositoryInterface::class) + ->willReturn($this->prophesize(AuthCodeRepositoryInterface::class)->reveal()); + + $result = $this->trait->proxy('getAuthCodeRepository', $this->container->reveal()); + $this->assertInstanceOf(AuthCodeRepositoryInterface::class, $result); + } +} diff --git a/test/ResourceServerFactoryTest.php b/test/ResourceServerFactoryTest.php index ad3be93..085c4a0 100644 --- a/test/ResourceServerFactoryTest.php +++ b/test/ResourceServerFactoryTest.php @@ -27,7 +27,7 @@ class ResourceServerFactoryTest extends TestCase 'key_permissions_check' => false, ]; - public function setUp() + protected function setUp() : void { $this->container = $this->prophesize(ContainerInterface::class); } @@ -48,9 +48,6 @@ public function testInvokeWithEmptyConfig() $factory($this->container->reveal()); } - /** - * @expectedException Zend\Expressive\Authentication\OAuth2\Exception\InvalidConfigException - */ public function testInvokeWithConfigWithoutRepository() { $this->container->has('config')->willReturn(true); @@ -64,6 +61,8 @@ public function testInvokeWithConfigWithoutRepository() ->willReturn(false); $factory = new ResourceServerFactory(); + + $this->expectException(Exception\InvalidConfigException::class); $factory($this->container->reveal()); } diff --git a/test/TokenEndpointHandlerFactoryTest.php b/test/TokenEndpointHandlerFactoryTest.php new file mode 100644 index 0000000..3f05a0a --- /dev/null +++ b/test/TokenEndpointHandlerFactoryTest.php @@ -0,0 +1,75 @@ +subject = new TokenEndpointHandlerFactory(); + parent::setUp(); + } + + public function testEmptyContainerThrowsTypeError() + { + $container = $this->prophesize(ContainerInterface::class); + + $this->expectException(TypeError::class); + ($this->subject)($container); + } + + public function testCreatesTokenEndpointHandler() + { + $server = $this->prophesize(AuthorizationServer::class); + $responseFactory = function () { + }; + $container = $this->prophesize(ContainerInterface::class); + + $container->get(AuthorizationServer::class) + ->willReturn($server->reveal()); + $container->get(ResponseInterface::class) + ->willReturn($responseFactory); + + self::assertInstanceOf(TokenEndpointHandler::class, ($this->subject)($container->reveal())); + } + + public function testDirectResponseInstanceFromContainerThrowsTypeError() + { + $server = $this->prophesize(AuthorizationServer::class); + $container = $this->prophesize(ContainerInterface::class); + + $container->get(AuthorizationServer::class) + ->willReturn($server->reveal()); + $container->get(ResponseInterface::class) + ->willReturn($this->prophesize(ResponseInterface::class)->reveal()); + + $this->expectException(TypeError::class); + ($this->subject)($container->reveal()); + } +} diff --git a/test/TokenEndpointHandlerTest.php b/test/TokenEndpointHandlerTest.php new file mode 100644 index 0000000..99833e9 --- /dev/null +++ b/test/TokenEndpointHandlerTest.php @@ -0,0 +1,82 @@ +prophesize(ResponseInterface::class)->reveal(); + }; + } + + public function testHandleUsesAuthorizationServer() + { + $server = $this->prophesize(AuthorizationServer::class); + $request = $this->prophesize(ServerRequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $expectedResponse = $response->reveal(); + + $server->respondToAccessTokenRequest($request->reveal(), $expectedResponse) + ->shouldBeCalled() + ->willReturn($expectedResponse); + + $subject = new TokenEndpointHandler($server->reveal(), $this->createResponseFactory($expectedResponse)); + self::assertSame($expectedResponse, $subject->handle($request->reveal())); + } + + public function testOAuthExceptionProducesResult() + { + $server = $this->prophesize(AuthorizationServer::class); + $request = $this->prophesize(ServerRequestInterface::class); + $response = $this->prophesize(ResponseInterface::class); + $exception = $this->prophesize(OAuthServerException::class); + $expectedResponse = $response->reveal(); + + $server->respondToAccessTokenRequest(Argument::cetera()) + ->willThrow($exception->reveal()); + + $exception->generateHttpResponse($expectedResponse, Argument::cetera()) + ->shouldBeCalled() + ->willReturn($expectedResponse); + + $subject = new TokenEndpointHandler($server->reveal(), $this->createResponseFactory($expectedResponse)); + self::assertSame($expectedResponse, $subject->handle($request->reveal())); + } + + public function testGenericExceptionsFallsThrough() + { + $server = $this->prophesize(AuthorizationServer::class); + $request = $this->prophesize(ServerRequestInterface::class); + $exception = new RuntimeException(); + + $server->respondToAccessTokenRequest(Argument::cetera()) + ->willThrow($exception); + + $subject = new TokenEndpointHandler($server->reveal(), $this->createResponseFactory()); + + $this->expectException(RuntimeException::class); + $subject->handle($request->reveal()); + } +}