From 3fbb6c6a2518cd5fd67cc433253db360c7d666a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Sch=C3=A4dlich?= Date: Sat, 30 Sep 2023 09:21:26 +0200 Subject: [PATCH] code challenge 10 solution --- .env | 6 + .gitignore | 4 + CODING-CHALLENGE-11.md | 30 + Makefile | 1 + composer.json | 2 + composer.lock | 1002 ++++++++++++++++- config/bundles.php | 2 + config/packages/lexik_jwt_authentication.yaml | 4 + config/packages/security.yaml | 66 ++ src/Controller/Attendee/CreateController.php | 2 + src/Controller/Attendee/DeleteController.php | 2 + src/Controller/Attendee/ReadController.php | 2 + src/Controller/Attendee/UpdateController.php | 2 + src/Controller/TokenController.php | 36 + .../AddAttendeeToWorkshopController.php | 2 + src/Controller/Workshop/CreateController.php | 2 + src/Controller/Workshop/DeleteController.php | 2 + src/Controller/Workshop/ReadController.php | 2 + .../RemoveAttendeeFromWorkshopController.php | 2 + src/Controller/Workshop/UpdateController.php | 2 + src/EventListener/ExceptionListener.php | 13 + src/Security/JwtTokenAuthenticator.php | 87 ++ symfony.lock | 24 + tests/ApiTestCase.php | 20 + .../Attendee/CreateControllerTest.php | 4 +- .../Attendee/DeleteControllerTest.php | 2 +- .../Attendee/ReadControllerTest.php | 1 + .../Attendee/UpdateControllerTest.php | 2 +- .../AddAttendeeToWorkshopControllerTest.php | 2 +- .../Workshop/CreateControllerTest.php | 4 +- .../Workshop/DeleteControllerTest.php | 2 +- .../Workshop/ReadControllerTest.php | 1 + ...RemoveAttendeeToWorkshopControllerTest.php | 2 +- .../Workshop/UpdateControllerTest.php | 4 +- 34 files changed, 1297 insertions(+), 44 deletions(-) create mode 100644 CODING-CHALLENGE-11.md create mode 100644 config/packages/lexik_jwt_authentication.yaml create mode 100644 config/packages/security.yaml create mode 100644 src/Controller/TokenController.php create mode 100644 src/Security/JwtTokenAuthenticator.php diff --git a/.env b/.env index 4906f46..6cc531c 100644 --- a/.env +++ b/.env @@ -26,3 +26,9 @@ DATABASE_URL=sqlite:///%kernel.project_dir%/var/data.db # DATABASE_URL="mysql://db_user:db_password@127.0.0.1:3306/db_name?serverVersion=5.7" # DATABASE_URL="postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=13&charset=utf8" ###< doctrine/doctrine-bundle ### + +###> lexik/jwt-authentication-bundle ### +JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem +JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +JWT_PASSPHRASE=cde703ea51cc6b270df31f4c8232d98f23da500b8b232252758dcd1b33b22c5b +###< lexik/jwt-authentication-bundle ### diff --git a/.gitignore b/.gitignore index b59eb57..26cbc9d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ composer.lock /phpunit.xml .phpunit.cache ###< phpunit/phpunit ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### diff --git a/CODING-CHALLENGE-11.md b/CODING-CHALLENGE-11.md new file mode 100644 index 0000000..6c97765 --- /dev/null +++ b/CODING-CHALLENGE-11.md @@ -0,0 +1,30 @@ +# RESTful Webservices in Symfony + +## Coding Challenge 11 - API Documentation + +### Tasks + +Let's document our API using the Nelmio ApiDocBundle. + +### Solution + +- require Nelmio's ApiDocBundle: `composer req nelmio/api-doc-bundle` +- adjust the bundle's configuration: + +```yaml +nelmio_api_doc: + documentation: + info: + title: RESTful Webservices in Symfony + description: "Workshop: RESTful Webservices in Symfony!" + version: 1.0.0 + areas: + path_patterns: + - ^/api(?!/doc$) # Accepts routes under /api except /api/doc + - ^/workshops + - ^/attendees +``` + +- add some annotation to your controllers, check https://github.com/zircote/swagger-php/tree/master/Examples for examples +- to be able to see the api documentation website, you need to install Twig and the Asset component `composer require twig asset` +- and adjust the routing configuration for the `nelmio/api-doc-bundle` in `config/routes/nelmio_api_doc.yaml` diff --git a/Makefile b/Makefile index 81dab38..93855f9 100755 --- a/Makefile +++ b/Makefile @@ -12,6 +12,7 @@ dev-init: # $(SYMFONY_CMD) doctrine:database:create $(SYMFONY_CMD) doctrine:schema:create $(SYMFONY_CMD) hautelook:fixtures:load -n --no-bundles + $(SYMFONY_CMD) lexik:jwt:generate-keypair --overwrite -q fixtures: ## load fixtures $(SYMFONY_CMD) hautelook:fixtures:load -n -vvv --no-bundles diff --git a/composer.json b/composer.json index 47c0ce1..8900a78 100644 --- a/composer.json +++ b/composer.json @@ -10,6 +10,7 @@ "doctrine/doctrine-bundle": "^2.10", "doctrine/doctrine-migrations-bundle": "^3.2", "doctrine/orm": "^2.16", + "lexik/jwt-authentication-bundle": "^2.19", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.24", "ramsey/uuid": "*", @@ -22,6 +23,7 @@ "symfony/property-access": "6.3.*", "symfony/property-info": "6.3.*", "symfony/runtime": "6.3.*", + "symfony/security-bundle": "6.3.*", "symfony/serializer": "6.3.*", "symfony/validator": "6.3.*", "symfony/yaml": "6.3.*", diff --git a/composer.lock b/composer.lock index 0e12fda..f332e71 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f9426abf25134c76aff2e86030cd7899", + "content-hash": "ac54d56370ac828b2202fa578f49ea22", "packages": [ { "name": "brick/math", @@ -1375,6 +1375,331 @@ }, "time": "2022-05-23T21:33:49+00:00" }, + { + "name": "lcobucci/clock", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "30a854ceb22bd87d83a7a4563b3f6312453945fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/30a854ceb22bd87d83a7a4563b3f6312453945fc", + "reference": "30a854ceb22bd87d83a7a4563b3f6312453945fc", + "shasum": "" + }, + "require": { + "php": "~8.2.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.26", + "lcobucci/coding-standard": "^10.0.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.0.17" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.1.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2023-03-20T19:12:25+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", + "reference": "47bdb0e0b5d00c2f89ebe33e7e384c77e84e7c34", + "shasum": "" + }, + "require": { + "ext-hash": "*", + "ext-json": "*", + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.1.0 || ~8.2.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.26.19", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^9.0", + "phpbench/phpbench": "^1.2.8", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.3", + "phpstan/phpstan-deprecation-rules": "^1.1.2", + "phpstan/phpstan-phpunit": "^1.3.8", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.0.12" + }, + "suggest": { + "lcobucci/clock": ">= 3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2023-02-25T21:35:16+00:00" + }, + { + "name": "lexik/jwt-authentication-bundle", + "version": "v2.19.1", + "source": { + "type": "git", + "url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git", + "reference": "2db3658bcb7902b63f09f23ebbefa77a94d3f55d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/2db3658bcb7902b63f09f23ebbefa77a94d3f55d", + "reference": "2db3658bcb7902b63f09f23ebbefa77a94d3f55d", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "lcobucci/clock": "^1.2|^2.0|^3.0", + "lcobucci/jwt": "^3.4|^4.1|^5.0", + "namshi/jose": "^7.2", + "php": ">=7.1", + "symfony/config": "^4.4|^5.3|^6.0", + "symfony/dependency-injection": "^4.4|^5.3|^6.0", + "symfony/deprecation-contracts": "^2.4|^3.0", + "symfony/event-dispatcher": "^4.4|^5.3|^6.0", + "symfony/http-foundation": "^4.4|^5.3|^6.0", + "symfony/http-kernel": "^4.4|^5.3|^6.0", + "symfony/property-access": "^4.4|^5.3|^6.0", + "symfony/security-bundle": "^4.4|^5.3|^6.0", + "symfony/translation-contracts": "^1.0|^2.0|^3.0" + }, + "conflict": { + "symfony/console": "<4.4" + }, + "require-dev": { + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^4.4|^5.3|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/filesystem": "^4.4|^5.3|^6.0", + "symfony/framework-bundle": "^4.4|^5.3|^6.0", + "symfony/phpunit-bridge": "^4.4|^5.3|^6.0", + "symfony/security-guard": "^4.4|^5.3", + "symfony/var-dumper": "^4.4|^5.3|^6.0", + "symfony/yaml": "^4.4|^5.3|^6.0" + }, + "suggest": { + "gesdinet/jwt-refresh-token-bundle": "Implements a refresh token system over Json Web Tokens in Symfony", + "spomky-labs/lexik-jose-bridge": "Provides a JWT Token encoder with encryption support" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Lexik\\Bundle\\JWTAuthenticationBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeremy Barthe", + "email": "j.barthe@lexik.fr", + "homepage": "https://github.com/jeremyb" + }, + { + "name": "Nicolas Cabot", + "email": "n.cabot@lexik.fr", + "homepage": "https://github.com/slashfan" + }, + { + "name": "Cedric Girard", + "email": "c.girard@lexik.fr", + "homepage": "https://github.com/cedric-g" + }, + { + "name": "Dev Lexik", + "email": "dev@lexik.fr", + "homepage": "https://github.com/lexik" + }, + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com", + "homepage": "https://github.com/chalasr" + }, + { + "name": "Lexik Community", + "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle/graphs/contributors" + } + ], + "description": "This bundle provides JWT authentication for your Symfony REST API", + "homepage": "https://github.com/lexik/LexikJWTAuthenticationBundle", + "keywords": [ + "Authentication", + "JWS", + "api", + "bundle", + "jwt", + "rest", + "symfony" + ], + "support": { + "issues": "https://github.com/lexik/LexikJWTAuthenticationBundle/issues", + "source": "https://github.com/lexik/LexikJWTAuthenticationBundle/tree/v2.19.1" + }, + "funding": [ + { + "url": "https://github.com/chalasr", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle", + "type": "tidelift" + } + ], + "time": "2023-07-04T01:04:21+00:00" + }, + { + "name": "namshi/jose", + "version": "7.2.3", + "source": { + "type": "git", + "url": "https://github.com/namshi/jose.git", + "reference": "89a24d7eb3040e285dd5925fcad992378b82bcff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/namshi/jose/zipball/89a24d7eb3040e285dd5925fcad992378b82bcff", + "reference": "89a24d7eb3040e285dd5925fcad992378b82bcff", + "shasum": "" + }, + "require": { + "ext-date": "*", + "ext-hash": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-spl": "*", + "php": ">=5.5", + "symfony/polyfill-php56": "^1.0" + }, + "require-dev": { + "phpseclib/phpseclib": "^2.0", + "phpunit/phpunit": "^4.5|^5.0", + "satooshi/php-coveralls": "^1.0" + }, + "suggest": { + "ext-openssl": "Allows to use OpenSSL as crypto engine.", + "phpseclib/phpseclib": "Allows to use Phpseclib as crypto engine, use version ^2.0." + }, + "type": "library", + "autoload": { + "psr-4": { + "Namshi\\JOSE\\": "src/Namshi/JOSE/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Nadalin", + "email": "alessandro.nadalin@gmail.com" + }, + { + "name": "Alessandro Cinelli (cirpo)", + "email": "alessandro.cinelli@gmail.com" + } + ], + "description": "JSON Object Signing and Encryption library for PHP.", + "keywords": [ + "JSON Web Signature", + "JSON Web Token", + "JWS", + "json", + "jwt", + "token" + ], + "support": { + "issues": "https://github.com/namshi/jose/issues", + "source": "https://github.com/namshi/jose/tree/master" + }, + "time": "2016-12-05T07:27:31+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1639,6 +1964,54 @@ }, "time": "2021-02-03T23:26:27+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -2233,40 +2606,33 @@ "time": "2023-05-23T14:45:45+00:00" }, { - "name": "symfony/config", - "version": "v6.3.2", + "name": "symfony/clock", + "version": "v6.3.4", "source": { "type": "git", - "url": "https://github.com/symfony/config.git", - "reference": "b47ca238b03e7b0d7880ffd1cf06e8d637ca1467" + "url": "https://github.com/symfony/clock.git", + "reference": "a74086d3db70d0f06ffd84480daa556248706e98" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/b47ca238b03e7b0d7880ffd1cf06e8d637ca1467", - "reference": "b47ca238b03e7b0d7880ffd1cf06e8d637ca1467", + "url": "https://api.github.com/repos/symfony/clock/zipball/a74086d3db70d0f06ffd84480daa556248706e98", + "reference": "a74086d3db70d0f06ffd84480daa556248706e98", "shasum": "" }, "require": { "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^5.4|^6.0", - "symfony/polyfill-ctype": "~1.8" - }, - "conflict": { - "symfony/finder": "<5.4", - "symfony/service-contracts": "<2.5" + "psr/clock": "^1.0" }, - "require-dev": { - "symfony/event-dispatcher": "^5.4|^6.0", - "symfony/finder": "^5.4|^6.0", - "symfony/messenger": "^5.4|^6.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^5.4|^6.0" + "provide": { + "psr/clock-implementation": "1.0" }, "type": "library", "autoload": { + "files": [ + "Resources/now.php" + ], "psr-4": { - "Symfony\\Component\\Config\\": "" + "Symfony\\Component\\Clock\\": "" }, "exclude-from-classmap": [ "/Tests/" @@ -2278,18 +2644,23 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "description": "Decouples applications from the system clock", "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], "support": { - "source": "https://github.com/symfony/config/tree/v6.3.2" + "source": "https://github.com/symfony/clock/tree/v6.3.4" }, "funding": [ { @@ -2305,26 +2676,101 @@ "type": "tidelift" } ], - "time": "2023-07-19T20:22:16+00:00" + "time": "2023-07-31T11:35:03+00:00" }, { - "name": "symfony/console", - "version": "v6.3.4", + "name": "symfony/config", + "version": "v6.3.2", "source": { "type": "git", - "url": "https://github.com/symfony/console.git", - "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" + "url": "https://github.com/symfony/config.git", + "reference": "b47ca238b03e7b0d7880ffd1cf06e8d637ca1467" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", - "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", + "url": "https://api.github.com/repos/symfony/config/zipball/b47ca238b03e7b0d7880ffd1cf06e8d637ca1467", + "reference": "b47ca238b03e7b0d7880ffd1cf06e8d637ca1467", "shasum": "" }, "require": { "php": ">=8.1", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/polyfill-mbstring": "~1.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<5.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/finder": "^5.4|^6.0", + "symfony/messenger": "^5.4|^6.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v6.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-19T20:22:16+00:00" + }, + { + "name": "symfony/console", + "version": "v6.3.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/eca495f2ee845130855ddf1cf18460c38966c8b6", + "reference": "eca495f2ee845130855ddf1cf18460c38966c8b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", "symfony/string": "^5.4|^6.0" }, @@ -3549,6 +3995,78 @@ ], "time": "2023-08-26T13:54:49+00:00" }, + { + "name": "symfony/password-hasher", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/password-hasher.git", + "reference": "278d3a49715073879f75e372ad80b8cfeca949d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/password-hasher/zipball/278d3a49715073879f75e372ad80b8cfeca949d3", + "reference": "278d3a49715073879f75e372ad80b8cfeca949d3", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "conflict": { + "symfony/security-core": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0", + "symfony/security-core": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PasswordHasher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides password hashing utilities", + "homepage": "https://symfony.com", + "keywords": [ + "hashing", + "password" + ], + "support": { + "source": "https://github.com/symfony/password-hasher/tree/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-25T17:05:16+00:00" + }, { "name": "symfony/polyfill-intl-grapheme", "version": "v1.28.0", @@ -3797,6 +4315,74 @@ ], "time": "2023-07-28T09:04:16+00:00" }, + { + "name": "symfony/polyfill-php56", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php56.git", + "reference": "54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675", + "reference": "54b8cd7e6c1643d78d011f3be89f3ef1f9f4c675", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 5.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php56/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" + }, { "name": "symfony/polyfill-php83", "version": "v1.28.0", @@ -4199,6 +4785,356 @@ ], "time": "2023-07-16T17:05:46+00:00" }, + { + "name": "symfony/security-bundle", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "2df460eacceb11b9287cfafddda4d27023dd9001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/2df460eacceb11b9287cfafddda4d27023dd9001", + "reference": "2df460eacceb11b9287cfafddda4d27023dd9001", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.1", + "symfony/clock": "^6.3", + "symfony/config": "^6.1", + "symfony/dependency-injection": "^6.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/http-foundation": "^6.2", + "symfony/http-kernel": "^6.2", + "symfony/password-hasher": "^5.4|^6.0", + "symfony/security-core": "^6.2", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/security-http": "^6.3.4" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/console": "<5.4", + "symfony/framework-bundle": "<6.3", + "symfony/http-client": "<5.4", + "symfony/ldap": "<5.4", + "symfony/twig-bundle": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.10.4|^2", + "symfony/asset": "^5.4|^6.0", + "symfony/browser-kit": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0", + "symfony/css-selector": "^5.4|^6.0", + "symfony/dom-crawler": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/form": "^5.4|^6.0", + "symfony/framework-bundle": "^6.3", + "symfony/http-client": "^5.4|^6.0", + "symfony/ldap": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/serializer": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/twig-bridge": "^5.4|^6.0", + "symfony/twig-bundle": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "twig/twig": "^2.13|^3.0.4", + "web-token/jwt-checker": "^3.1", + "web-token/jwt-signature-algorithm-ecdsa": "^3.1", + "web-token/jwt-signature-algorithm-eddsa": "^3.1", + "web-token/jwt-signature-algorithm-hmac": "^3.1", + "web-token/jwt-signature-algorithm-none": "^3.1", + "web-token/jwt-signature-algorithm-rsa": "^3.1" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-25T17:05:55+00:00" + }, + { + "name": "symfony/security-core", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-core.git", + "reference": "ec8f24dc1195f46483510892271d01a5202bba70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-core/zipball/ec8f24dc1195f46483510892271d01a5202bba70", + "reference": "ec8f24dc1195f46483510892271d01a5202bba70", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3", + "symfony/password-hasher": "^5.4|^6.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/event-dispatcher": "<5.4", + "symfony/http-foundation": "<5.4", + "symfony/ldap": "<5.4", + "symfony/security-guard": "<5.4", + "symfony/validator": "<5.4" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "psr/container": "^1.1|^2.0", + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/event-dispatcher": "^5.4|^6.0", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/ldap": "^5.4|^6.0", + "symfony/string": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "symfony/validator": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Core\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - Core Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-core/tree/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-09-10T17:47:23+00:00" + }, + { + "name": "symfony/security-csrf", + "version": "v6.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-csrf.git", + "reference": "63d7b098c448cbddb46ea5eda33b68c1ece6eb5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-csrf/zipball/63d7b098c448cbddb46ea5eda33b68c1ece6eb5b", + "reference": "63d7b098c448cbddb46ea5eda33b68c1ece6eb5b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/security-core": "^5.4|^6.0" + }, + "conflict": { + "symfony/http-foundation": "<5.4" + }, + "require-dev": { + "symfony/http-foundation": "^5.4|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Csrf\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - CSRF Library", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-csrf/tree/v6.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-05T08:41:27+00:00" + }, + { + "name": "symfony/security-http", + "version": "v6.3.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "47058ea557a4c64ba86e9249651222842bd52e2a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/47058ea557a4c64ba86e9249651222842bd52e2a", + "reference": "47058ea557a4c64ba86e9249651222842bd52e2a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-foundation": "^5.4|^6.0", + "symfony/http-kernel": "^6.3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^5.4|^6.0", + "symfony/security-core": "^6.3" + }, + "conflict": { + "symfony/clock": "<6.3", + "symfony/event-dispatcher": "<5.4.9|>=6,<6.0.9", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<5.4", + "symfony/security-csrf": "<5.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^5.4|^6.0", + "symfony/clock": "^6.3", + "symfony/expression-language": "^5.4|^6.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^5.4|^6.0", + "symfony/routing": "^5.4|^6.0", + "symfony/security-csrf": "^5.4|^6.0", + "symfony/translation": "^5.4|^6.0", + "web-token/jwt-checker": "^3.1", + "web-token/jwt-signature-algorithm-ecdsa": "^3.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v6.3.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-08-30T06:30:46+00:00" + }, { "name": "symfony/serializer", "version": "v6.3.4", diff --git a/config/bundles.php b/config/bundles.php index c085db4..4312488 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -7,4 +7,6 @@ Nelmio\Alice\Bridge\Symfony\NelmioAliceBundle::class => ['dev' => true, 'test' => true], Fidry\AliceDataFixtures\Bridge\Symfony\FidryAliceDataFixturesBundle::class => ['dev' => true, 'test' => true], Hautelook\AliceBundle\HautelookAliceBundle::class => ['dev' => true, 'test' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], + Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true], ]; diff --git a/config/packages/lexik_jwt_authentication.yaml b/config/packages/lexik_jwt_authentication.yaml new file mode 100644 index 0000000..edfb69d --- /dev/null +++ b/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,4 @@ +lexik_jwt_authentication: + secret_key: '%env(resolve:JWT_SECRET_KEY)%' + public_key: '%env(resolve:JWT_PUBLIC_KEY)%' + pass_phrase: '%env(JWT_PASSPHRASE)%' diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000..814417d --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,66 @@ +security: + + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + + # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: + in_memory: + memory: + users: + # use bin/console security:hash-password to hash a different password + api_user: { password: '$2y$13$6Cc1CLoERFozkNdqHceBEeYdzZpyef2FEQovaqTbcKgF9hAATx89m', roles: 'ROLE_USER' } # password: api_user + api_admin: { password: '$2y$13$aWQ/vgmJFbZpfPEqCR8wxeh1AKEVu6ieSqUjO4NoBsNl0i8T4oXwG', roles: 'ROLE_ADMIN' } # password: api_admin + + role_hierarchy: + ROLE_ADMIN: ROLE_USER + + # use bin/console debug:firewall for debugging + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + token: + lazy: true + stateless: true + pattern: ^/token + http_basic: true + provider: in_memory + api: + lazy: true + stateless: true + pattern: ^/(workshops|attendees) + custom_authenticators: + - App\Security\JwtTokenAuthenticator + entry_point: App\Security\JwtTokenAuthenticator + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#the-firewall + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + #- { path: ^/token, roles: IS_AUTHENTICATED_FULLY } + #- { path: ^/(workshops|attendees), roles: PUBLIC_ACCESS } + #- { path: ^/(workshops|attendees)/\d+$, roles: IS_AUTHENTICATED_FULLY } + #- { path: ^/(workshops|attendees)/\d+$, roles: ROLE_USER, methods: [GET, POST, PUT]} + #- { path: ^/(workshops|attendees)/\d+$, roles: ROLE_ADMIN, methods: [DELETE]} + #- { path: ^/workshops/\d+/attendees/add/d+$, roles: ROLE_USER} + #- { path: ^/workshops/\d+/attendees/remove/d+$, roles: ROLE_ADMIN} + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # important to generate secure password hashes. In tests however, secure hashes + # are not important, waste resources and increase test times. The following + # reduces the work factor to the lowest possible values. + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/src/Controller/Attendee/CreateController.php b/src/Controller/Attendee/CreateController.php index 2e8abe3..14e2dfd 100644 --- a/src/Controller/Attendee/CreateController.php +++ b/src/Controller/Attendee/CreateController.php @@ -10,8 +10,10 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; +#[IsGranted('ROLE_USER')] #[Route('/attendees', name: 'create_attendee', methods: ['POST'])] final class CreateController { diff --git a/src/Controller/Attendee/DeleteController.php b/src/Controller/Attendee/DeleteController.php index 094c35c..4f52198 100644 --- a/src/Controller/Attendee/DeleteController.php +++ b/src/Controller/Attendee/DeleteController.php @@ -9,7 +9,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +#[IsGranted('ROLE_ADMIN')] #[Route('/attendees/{identifier}', name: 'delete_attendee', methods: ['DELETE'])] class DeleteController { diff --git a/src/Controller/Attendee/ReadController.php b/src/Controller/Attendee/ReadController.php index ed626de..7c00636 100644 --- a/src/Controller/Attendee/ReadController.php +++ b/src/Controller/Attendee/ReadController.php @@ -8,8 +8,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; +#[IsGranted('ROLE_USER')] #[Route('/attendees/{identifier}', name: 'read_attendee', methods: ['GET'])] final class ReadController { diff --git a/src/Controller/Attendee/UpdateController.php b/src/Controller/Attendee/UpdateController.php index b63a633..c64717a 100644 --- a/src/Controller/Attendee/UpdateController.php +++ b/src/Controller/Attendee/UpdateController.php @@ -10,7 +10,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +#[IsGranted('ROLE_USER')] #[Route('/attendees/{identifier}', name: 'update_attendee', methods: ['PUT'])] final class UpdateController { diff --git a/src/Controller/TokenController.php b/src/Controller/TokenController.php new file mode 100644 index 0000000..3e20edc --- /dev/null +++ b/src/Controller/TokenController.php @@ -0,0 +1,36 @@ +jwtEncoder->encode([ + 'username' => $request->getUser(), + ]); + + $data = ['token' => $token]; + + $serializedData = $this->serializer->serialize($data, $request->getRequestFormat()); + + return new Response($serializedData, Response::HTTP_OK); + } +} diff --git a/src/Controller/Workshop/AddAttendeeToWorkshopController.php b/src/Controller/Workshop/AddAttendeeToWorkshopController.php index eeed716..39a91ab 100644 --- a/src/Controller/Workshop/AddAttendeeToWorkshopController.php +++ b/src/Controller/Workshop/AddAttendeeToWorkshopController.php @@ -11,7 +11,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +#[IsGranted('ROLE_USER')] #[Route('/workshops/{identifier}/attendees/add/{attendee_identifier}', name: 'add_attendee_to_workshop', methods: ['POST'])] class AddAttendeeToWorkshopController { diff --git a/src/Controller/Workshop/CreateController.php b/src/Controller/Workshop/CreateController.php index bf7dc13..8d43359 100644 --- a/src/Controller/Workshop/CreateController.php +++ b/src/Controller/Workshop/CreateController.php @@ -10,8 +10,10 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; +#[IsGranted('ROLE_USER')] #[Route('/workshops', name: 'create_workshop', methods: ['POST'])] final class CreateController { diff --git a/src/Controller/Workshop/DeleteController.php b/src/Controller/Workshop/DeleteController.php index 16d2742..2a92cbe 100644 --- a/src/Controller/Workshop/DeleteController.php +++ b/src/Controller/Workshop/DeleteController.php @@ -9,7 +9,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +#[IsGranted('ROLE_ADMIN')] #[Route('/workshops/{identifier}', name: 'delete_workshop', methods: ['DELETE'])] class DeleteController { diff --git a/src/Controller/Workshop/ReadController.php b/src/Controller/Workshop/ReadController.php index 3bed2ee..8bbc32e 100644 --- a/src/Controller/Workshop/ReadController.php +++ b/src/Controller/Workshop/ReadController.php @@ -8,8 +8,10 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; use Symfony\Component\Serializer\SerializerInterface; +#[IsGranted('ROLE_USER')] #[Route('/workshops/{identifier}', name: 'read_workshop', methods: ['GET'])] final class ReadController { diff --git a/src/Controller/Workshop/RemoveAttendeeFromWorkshopController.php b/src/Controller/Workshop/RemoveAttendeeFromWorkshopController.php index a7fd652..3130c5f 100644 --- a/src/Controller/Workshop/RemoveAttendeeFromWorkshopController.php +++ b/src/Controller/Workshop/RemoveAttendeeFromWorkshopController.php @@ -11,7 +11,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +#[IsGranted('ROLE_USER')] #[Route('/workshops/{identifier}/attendees/remove/{attendee_identifier}', name: 'remove_attendee_from_workshop', methods: ['POST'])] class RemoveAttendeeFromWorkshopController { diff --git a/src/Controller/Workshop/UpdateController.php b/src/Controller/Workshop/UpdateController.php index 4dcad62..9488430 100644 --- a/src/Controller/Workshop/UpdateController.php +++ b/src/Controller/Workshop/UpdateController.php @@ -10,7 +10,9 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; +#[IsGranted('ROLE_USER')] #[Route('/workshops/{identifier}', name: 'update_workshop', methods: ['PUT'])] final class UpdateController { diff --git a/src/EventListener/ExceptionListener.php b/src/EventListener/ExceptionListener.php index 21a6284..4c27d82 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/EventListener/ExceptionListener.php @@ -11,6 +11,7 @@ use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ExceptionEvent; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Serializer\Exception\NotEncodableValueException; use Symfony\Component\Serializer\SerializerInterface; @@ -30,6 +31,7 @@ public function onKernelException(ExceptionEvent $event): void $throwable = $event->getThrowable(); if (!($throwable instanceof NotEncodableValueException + || $throwable instanceof AccessDeniedHttpException || $throwable instanceof ValidationFailedException || $throwable instanceof AttendeeAlreadyAttendsOtherWorkshopOnThatDateException || $throwable instanceof AttendeeLimitReachedException) @@ -37,6 +39,17 @@ public function onKernelException(ExceptionEvent $event): void return; } + if ($throwable instanceof AccessDeniedHttpException) { + $errorCollection = (new ApiErrorCollection()) + ->addApiError(new ApiError('Access Denied.', $throwable->getMessage())); + + $serializedErrors = $this->serializer->serialize($errorCollection, $event->getRequest()->getRequestFormat()); + + $event->setResponse(new Response($serializedErrors, Response::HTTP_FORBIDDEN)); + + return; + } + if ($throwable instanceof NotEncodableValueException) { $errorCollection = (new ApiErrorCollection()) ->addApiError(new ApiError('Encoding failed.', $throwable->getMessage())); diff --git a/src/Security/JwtTokenAuthenticator.php b/src/Security/JwtTokenAuthenticator.php new file mode 100644 index 0000000..f333db6 --- /dev/null +++ b/src/Security/JwtTokenAuthenticator.php @@ -0,0 +1,87 @@ +tokenExtractor = new AuthorizationHeaderTokenExtractor( + 'Bearer', + 'Authorization' + ); + } + + public function start(Request $request, AuthenticationException $authException = null): Response + { + $error = new ApiError('Invalid credentials.', 'Authentication header required.'); + + $serializedError = $this->serializer->serialize($error, $request->getRequestFormat()); + + return new Response($serializedError, Response::HTTP_UNAUTHORIZED, [ + 'WWW-Authenticate' => 'Bearer', + ]); + } + + public function supports(Request $request): ?bool + { + return false !== $this->tokenExtractor->extract($request); + } + + public function authenticate(Request $request): Passport + { + $token = $this->tokenExtractor->extract($request); + + try { + $data = $this->jwtEncoder->decode($token); + } catch (JWTDecodeFailureException $decodeFailureException) { + throw new InvalidTokenException('Invalid JWT Token', 0, $decodeFailureException); + } + + return new SelfValidatingPassport( + new UserBadge($data['username'], function ($userIdentifier) { + return $this->userProvider->loadUserByIdentifier($userIdentifier); + }) + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $firewallName): ?Response + { + return null; + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response + { + $error = new ApiError('Invalid credentials.', 'Valid token required.'); + + $serializedError = $this->serializer->serialize($error, $request->getRequestFormat()); + + return new Response($serializedError, Response::HTTP_UNAUTHORIZED, [ + 'WWW-Authenticate' => 'Bearer', + ]); + } +} diff --git a/symfony.lock b/symfony.lock index 204fd77..51b7327 100644 --- a/symfony.lock +++ b/symfony.lock @@ -51,6 +51,18 @@ "fixtures/.gitignore" ] }, + "lexik/jwt-authentication-bundle": { + "version": "2.19", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, "nelmio/alice": { "version": "3.12", "recipe": { @@ -160,6 +172,18 @@ "config/routes.yaml" ] }, + "symfony/security-bundle": { + "version": "6.3", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.0", + "ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48" + }, + "files": [ + "config/packages/security.yaml" + ] + }, "symfony/validator": { "version": "6.3", "recipe": { diff --git a/tests/ApiTestCase.php b/tests/ApiTestCase.php index 1ff709d..7d881de 100644 --- a/tests/ApiTestCase.php +++ b/tests/ApiTestCase.php @@ -28,4 +28,24 @@ protected function setUp(): void $schemaTool->dropSchema($this->entityManager->getMetadataFactory()->getAllMetadata()); $schemaTool->createSchema($this->entityManager->getMetadataFactory()->getAllMetadata()); } + + protected function getUserToken(): string + { + $this->browser->request('POST', '/token', [], [], [ + 'PHP_AUTH_USER' => 'api_user', + 'PHP_AUTH_PW' => 'api_user', + ]); + + return json_decode($this->browser->getResponse()->getContent(), true)['token']; + } + + protected function getAdminToken(): string + { + $this->browser->request('POST', '/token', [], [], [ + 'PHP_AUTH_USER' => 'api_admin', + 'PHP_AUTH_PW' => 'api_admin', + ]); + + return json_decode($this->browser->getResponse()->getContent(), true)['token']; + } } diff --git a/tests/Controller/Attendee/CreateControllerTest.php b/tests/Controller/Attendee/CreateControllerTest.php index a014b9c..44c26db 100644 --- a/tests/Controller/Attendee/CreateControllerTest.php +++ b/tests/Controller/Attendee/CreateControllerTest.php @@ -15,7 +15,7 @@ public function test_it_should_create_an_attendee(): void $attendeesBefore = static::getContainer()->get(AttendeeRepository::class)->findAll(); static::assertCount(0, $attendeesBefore); - $this->browser->request('POST', '/attendees', [], [], [], + $this->browser->request('POST', '/attendees', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()], <<<'EOT' { "firstname": "Paul", @@ -44,7 +44,7 @@ public function test_it_should_create_an_attendee(): void */ public function test_it_should_throw_an_UnprocessableEntityHttpException(string $requestBody): void { - $this->browser->request('POST', '/attendees', [], [], [], $requestBody); + $this->browser->request('POST', '/attendees', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()], $requestBody); static::assertResponseStatusCodeSame(422); diff --git a/tests/Controller/Attendee/DeleteControllerTest.php b/tests/Controller/Attendee/DeleteControllerTest.php index f5e58f7..5f99889 100644 --- a/tests/Controller/Attendee/DeleteControllerTest.php +++ b/tests/Controller/Attendee/DeleteControllerTest.php @@ -19,7 +19,7 @@ public function test_it_should_delete_an_attendee(): void static::getContainer()->get(AttendeeRepository::class)->findOneByIdentifier('bb5cb8a8-0df8-404f-a3f3-54ee5c9cf855') ); - $this->browser->request('DELETE', '/attendees/bb5cb8a8-0df8-404f-a3f3-54ee5c9cf855'); + $this->browser->request('DELETE', '/attendees/bb5cb8a8-0df8-404f-a3f3-54ee5c9cf855', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getAdminToken()]); static::assertNull( static::getContainer()->get(AttendeeRepository::class)->findOneByIdentifier('bb5cb8a8-0df8-404f-a3f3-54ee5c9cf855') diff --git a/tests/Controller/Attendee/ReadControllerTest.php b/tests/Controller/Attendee/ReadControllerTest.php index c00142c..03b2e6d 100644 --- a/tests/Controller/Attendee/ReadControllerTest.php +++ b/tests/Controller/Attendee/ReadControllerTest.php @@ -25,6 +25,7 @@ public function test_it_should_show_requested_attendee(string $httpAcceptHeaderV $this->browser->request('GET', '/attendees/17058af8-1b0f-4afe-910d-669b4bd0fd26', [], [], [ 'HTTP_ACCEPT' => $httpAcceptHeaderValue, + 'HTTP_Authorization' => 'Bearer '.$this->getUserToken(), ]); static::assertResponseIsSuccessful(); diff --git a/tests/Controller/Attendee/UpdateControllerTest.php b/tests/Controller/Attendee/UpdateControllerTest.php index f11da78..8be94d1 100644 --- a/tests/Controller/Attendee/UpdateControllerTest.php +++ b/tests/Controller/Attendee/UpdateControllerTest.php @@ -19,7 +19,7 @@ public function test_it_should_update_an_attendee(): void $attendeesBefore = static::getContainer()->get(AttendeeRepository::class)->findAll(); static::assertCount(1, $attendeesBefore); - $this->browser->request('PUT', '/attendees/b38aa3e4-f9de-4dca-aaeb-3ec36a9feb6c', [], [], [], + $this->browser->request('PUT', '/attendees/b38aa3e4-f9de-4dca-aaeb-3ec36a9feb6c', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()], <<<'EOT' { "firstname": "Paul", diff --git a/tests/Controller/Workshop/AddAttendeeToWorkshopControllerTest.php b/tests/Controller/Workshop/AddAttendeeToWorkshopControllerTest.php index c266319..b335d0e 100644 --- a/tests/Controller/Workshop/AddAttendeeToWorkshopControllerTest.php +++ b/tests/Controller/Workshop/AddAttendeeToWorkshopControllerTest.php @@ -20,7 +20,7 @@ public function test_it_should_add_an_attendee_to_a_workshop(): void $workshop = static::getContainer()->get(WorkshopRepository::class)->findOneByIdentifier('e5444459-db7f-42a8-9a93-7925d4ffd1dc'); static::assertCount(0, $workshop->getAttendees()); - $this->browser->request('POST', '/workshops/e5444459-db7f-42a8-9a93-7925d4ffd1dc/attendees/add/e9a95edb-9b49-42f4-bf2d-7206fd65bc94', [], [], []); + $this->browser->request('POST', '/workshops/e5444459-db7f-42a8-9a93-7925d4ffd1dc/attendees/add/e9a95edb-9b49-42f4-bf2d-7206fd65bc94', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()]); static::assertResponseStatusCodeSame(204); diff --git a/tests/Controller/Workshop/CreateControllerTest.php b/tests/Controller/Workshop/CreateControllerTest.php index 94b669b..d57a7c5 100644 --- a/tests/Controller/Workshop/CreateControllerTest.php +++ b/tests/Controller/Workshop/CreateControllerTest.php @@ -15,7 +15,7 @@ public function test_it_should_create_a_workshop(): void $workshopsBefore = static::getContainer()->get(WorkshopRepository::class)->findAll(); static::assertCount(0, $workshopsBefore); - $this->browser->request('POST', '/workshops', [], [], [], + $this->browser->request('POST', '/workshops', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()], <<<'EOT' { "title": "Test Workshop", @@ -42,7 +42,7 @@ public function test_it_should_create_a_workshop(): void */ public function test_it_should_return_proper_errors(string $requestBody): void { - $this->browser->request('POST', '/workshops', [], [], [], $requestBody); + $this->browser->request('POST', '/workshops', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()], $requestBody); static::assertResponseStatusCodeSame(422); diff --git a/tests/Controller/Workshop/DeleteControllerTest.php b/tests/Controller/Workshop/DeleteControllerTest.php index eac1207..f28ea09 100644 --- a/tests/Controller/Workshop/DeleteControllerTest.php +++ b/tests/Controller/Workshop/DeleteControllerTest.php @@ -19,7 +19,7 @@ public function test_it_should_delete_a_workshop(): void static::getContainer()->get(WorkshopRepository::class)->findOneByIdentifier('bd5c7f16-576a-48ef-963b-c91b62e1942b') ); - $this->browser->request('DELETE', '/workshops/bd5c7f16-576a-48ef-963b-c91b62e1942b'); + $this->browser->request('DELETE', '/workshops/bd5c7f16-576a-48ef-963b-c91b62e1942b', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getAdminToken()]); static::assertNull( static::getContainer()->get(WorkshopRepository::class)->findOneByIdentifier('bd5c7f16-576a-48ef-963b-c91b62e1942b') diff --git a/tests/Controller/Workshop/ReadControllerTest.php b/tests/Controller/Workshop/ReadControllerTest.php index 70504e4..c38ee63 100644 --- a/tests/Controller/Workshop/ReadControllerTest.php +++ b/tests/Controller/Workshop/ReadControllerTest.php @@ -25,6 +25,7 @@ public function test_it_should_show_requested_workshop(string $httpAcceptHeaderV $this->browser->request('GET', '/workshops/8acf8f2b-95c1-46e1-85a4-ea6ff88081ce', [], [], [ 'HTTP_ACCEPT' => $httpAcceptHeaderValue, + 'HTTP_Authorization' => 'Bearer '.$this->getUserToken(), ]); static::assertResponseIsSuccessful(); diff --git a/tests/Controller/Workshop/RemoveAttendeeToWorkshopControllerTest.php b/tests/Controller/Workshop/RemoveAttendeeToWorkshopControllerTest.php index 74d6e71..1ee1797 100644 --- a/tests/Controller/Workshop/RemoveAttendeeToWorkshopControllerTest.php +++ b/tests/Controller/Workshop/RemoveAttendeeToWorkshopControllerTest.php @@ -20,7 +20,7 @@ public function test_it_should_add_an_attendee_to_a_workshop(): void $workshop = static::getContainer()->get(WorkshopRepository::class)->findOneByIdentifier('667731df-0a66-4030-9589-e8ab850a209b'); static::assertCount(1, $workshop->getAttendees()); - $this->browser->request('POST', '/workshops/667731df-0a66-4030-9589-e8ab850a209b/attendees/remove/f6ac0c74-ca77-4b3b-9829-8c0cfe29cf44', [], [], []); + $this->browser->request('POST', '/workshops/667731df-0a66-4030-9589-e8ab850a209b/attendees/remove/f6ac0c74-ca77-4b3b-9829-8c0cfe29cf44', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()]); static::assertResponseStatusCodeSame(204); diff --git a/tests/Controller/Workshop/UpdateControllerTest.php b/tests/Controller/Workshop/UpdateControllerTest.php index 4622d52..249f900 100644 --- a/tests/Controller/Workshop/UpdateControllerTest.php +++ b/tests/Controller/Workshop/UpdateControllerTest.php @@ -19,7 +19,7 @@ public function test_it_should_update_an_workshop(): void $workshopsBefore = static::getContainer()->get(WorkshopRepository::class)->findAll(); static::assertCount(1, $workshopsBefore); - $this->browser->request('PUT', '/workshops/74857cb7-ff9e-4976-87e5-168438c3c53e', [], [], [], + $this->browser->request('PUT', '/workshops/74857cb7-ff9e-4976-87e5-168438c3c53e', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()], <<<'EOT' { "title": "Test Workshop", @@ -51,7 +51,7 @@ public function test_it_should_return_proper_errors(string $requestBody): void $workshopsBefore = static::getContainer()->get(WorkshopRepository::class)->findAll(); static::assertCount(1, $workshopsBefore); - $this->browser->request('PUT', '/workshops/74857cb7-ff9e-4976-87e5-168438c3c53e', [], [], [], $requestBody); + $this->browser->request('PUT', '/workshops/74857cb7-ff9e-4976-87e5-168438c3c53e', [], [], ['HTTP_Authorization' => 'Bearer '.$this->getUserToken()], $requestBody); static::assertResponseStatusCodeSame(422);