diff --git a/.env b/.env index 32ab3c6..91e8ce4 100644 --- a/.env +++ b/.env @@ -39,3 +39,10 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###> symfony/mailer ### MAILER_DSN=null://null ###< symfony/mailer ### + +###> league/oauth2-server-bundle ### +OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/jwt/private.pem +OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem +OAUTH_PASSPHRASE=a3c39947511d0cb3a682f50041805922 +OAUTH_ENCRYPTION_KEY=cab3f0f4a9beaaa90a77ee55cf01790b +###< league/oauth2-server-bundle ### diff --git a/.gitignore b/.gitignore index 4daae38..294e396 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ /public/assets/ /assets/vendor/ ###< symfony/asset-mapper ### + +###> league/oauth2-server-bundle ### +/config/jwt/*.pem +###< league/oauth2-server-bundle ### diff --git a/composer.json b/composer.json index be710c0..15fc6d7 100644 --- a/composer.json +++ b/composer.json @@ -11,6 +11,7 @@ "doctrine/doctrine-bundle": "^2.13", "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", + "league/oauth2-server-bundle": "^0.10.0", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.1", "symfony/asset": "7.2.*", diff --git a/composer.lock b/composer.lock index 344fd99..cd5c59c 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": "eaa614d3c00b5654c4cc228dcad4b8c8", + "content-hash": "fb9a76b78492ad17c1bbef7b24b1578b", "packages": [ { "name": "composer/semver", @@ -87,6 +87,73 @@ ], "time": "2024-09-19T14:15:21+00:00" }, + { + "name": "defuse/php-encryption", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", + "yoast/phpunit-polyfills": "^2.0.0" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" + }, + "time": "2023-06-19T06:10:36+00:00" + }, { "name": "doctrine/cache", "version": "2.2.0", @@ -1368,6 +1435,547 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "lcobucci/clock", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "reference": "db3713a61addfffd615b79bf0bc22f0ccc61b86b", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/coding-standard": "^11.1.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.25", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", + "phpunit/phpunit": "^11.3.6" + }, + "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.3.1" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2024-09-24T20:45:14+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.5.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "a835af59b030d3f2967725697cf88300f579088e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/a835af59b030d3f2967725697cf88300f579088e", + "reference": "a835af59b030d3f2967725697cf88300f579088e", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.29", + "lcobucci/clock": "^3.2", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2", + "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": "^11.1" + }, + "suggest": { + "lcobucci/clock": ">= 3.2" + }, + "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.5.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2025-01-26T21:29:45+00:00" + }, + { + "name": "league/event", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/ec38ff7ea10cad7d99a79ac937fbcffb9334c210", + "reference": "ec38ff7ea10cad7d99a79ac937fbcffb9334c210", + "shasum": "" + }, + "require": { + "php": ">=7.2.0", + "psr/event-dispatcher": "^1.0" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "phpstan/phpstan": "^0.12.45", + "phpunit/phpunit": "^8.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/3.0.3" + }, + "time": "2024-09-04T16:06:53+00:00" + }, + { + "name": "league/oauth2-server", + "version": "9.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "00323013403e1a1e0f424affafca56c28b60c22c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/00323013403e1a1e0f424affafca56c28b60c22c", + "reference": "00323013403e1a1e0f424affafca56c28b60c22c", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.4", + "ext-json": "*", + "ext-openssl": "*", + "lcobucci/clock": "^2.3 || ^3.0", + "lcobucci/jwt": "^5.0", + "league/event": "^3.0", + "league/uri": "^7.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "psr/http-message": "^2.0", + "psr/http-server-middleware": "^1.0" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "laminas/laminas-diactoros": "^3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.2", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.12", + "phpstan/phpstan-deprecation-rules": "^1.1.4", + "phpstan/phpstan-phpunit": "^1.3.15", + "phpstan/phpstan-strict-rules": "^1.5.2", + "phpunit/phpunit": "^9.6.21", + "roave/security-advisories": "dev-master", + "slevomat/coding-standard": "^8.14.1", + "squizlabs/php_codesniffer": "^3.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "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.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-server/issues", + "source": "https://github.com/thephpleague/oauth2-server/tree/9.2.0" + }, + "funding": [ + { + "url": "https://github.com/sephster", + "type": "github" + } + ], + "time": "2025-02-15T00:49:10+00:00" + }, + { + "name": "league/oauth2-server-bundle", + "version": "v0.10", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server-bundle.git", + "reference": "c6f8fff9c17b60a4089ff688d78ba6131b56e765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server-bundle/zipball/c6f8fff9c17b60a4089ff688d78ba6131b56e765", + "reference": "c6f8fff9c17b60a4089ff688d78ba6131b56e765", + "shasum": "" + }, + "require": { + "doctrine/doctrine-bundle": "^2.8.0", + "doctrine/orm": "^2.14|^3.0", + "ext-openssl": "*", + "league/oauth2-server": "^9.2", + "nyholm/psr7": "^1.4", + "php": "^8.1", + "psr/http-factory": "^1.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/polyfill-php81": "^1.22", + "symfony/psr-http-message-bridge": "^6.4|^7", + "symfony/security-bundle": "^6.4|^7.0" + }, + "require-dev": { + "ext-pdo": "*", + "ext-pdo_sqlite": "*", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/phpunit-bridge": "^7.2" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "0.10-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Bundle\\OAuth2ServerBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robin Chalas", + "email": "robin.chalas@gmail.com" + }, + { + "name": "All contributors", + "homepage": "https://github.com/thephpleague/oauth2-server-bundle/graphs/contributors" + } + ], + "description": "Symfony bundle .", + "homepage": "https://github.com/thephpleague/oauth2-server-bundle", + "keywords": [ + "auth", + "authorization", + "bundle", + "oauth2", + "server" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-server-bundle/issues", + "source": "https://github.com/thephpleague/oauth2-server-bundle/tree/v0.10" + }, + "time": "2025-03-13T18:24:18+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, { "name": "monolog/monolog", "version": "3.8.1", @@ -1471,6 +2079,134 @@ ], "time": "2024-12-05T17:15:07+00:00" }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "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", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -1893,6 +2629,227 @@ }, "time": "2019-01-08T18:20:26+00:00" }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, { "name": "psr/link", "version": "2.0.1", @@ -5487,6 +6444,89 @@ ], "time": "2025-01-27T11:08:17+00:00" }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v7.2.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "reference": "03f2f72319e7acaf2a9f6fcbe30ef17eec51594f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/http-message": "^1.0|^2.0", + "symfony/http-foundation": "^6.4|^7.0" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "php-http/discovery": "^1.15", + "psr/log": "^1.1.4|^2|^3", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/framework-bundle": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0" + }, + "type": "symfony-bridge", + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "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": "PSR HTTP message bridge", + "homepage": "https://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v7.2.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": "2024-09-26T08:57:56+00:00" + }, { "name": "symfony/routing", "version": "v7.2.3", diff --git a/config/bundles.php b/config/bundles.php index 4e3a560..9fb635f 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,4 +13,5 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true], ]; diff --git a/config/oauth/private.key b/config/oauth/private.key new file mode 100644 index 0000000..ce8e905 --- /dev/null +++ b/config/oauth/private.key @@ -0,0 +1,30 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIKLNNtdUp7M4CAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAECBBDMDNcnYR56ZlMbvkfMLWUvBIIE +0CQZaKA3CQU2r2SMoVyKwLBgSQJicPwuYpRV0HdfEB/j8zZgQW4mv3N4kCvFbALx +1p6WPEHSekCqM1h2O1w4BPLfJMTC3PreEf+Mp+zxrJJsXPYZIk1nqCrGiW5Yz9u4 +eBfC9V4eBkL34nD+pQDl9PqKFenhX3ft0PjEhM1OYpVAiMoajtOz983qvW2bUbCn +NzLIcbuh4g5xDqagjpjFLesrfTwaplWsx8S/b/kAnlUP9kFIgx27mn/wBaHPnGN0 +YaPiQDtnZDJrEXiNF3LiEh846HPxqYpqlosoJLyulKqbXsvSiFmp6qXP4vnbX3fm +df8Rv/jExMATagS2UfZOIzy4Z3l6P9AkiBRzsvk0HHLHy2Phh3N3KO+n+p7pIwO9 +t1v4SlAPHQb8yr0WBgZXn/+83WNQl/PXk+Zf0TqcRQXoUQ3Vk4nt6bqXjm3l7Cu8 +P+l/j1gzfJ+YTxF7NcAvzE2z1uYVnQv4zMerZeoVmBBs4scuZBVjCZg6yZr2hZqw +ZvJs/PL95Zz9FRqbGBQpA+gybePiLXmMjiy91csbagcE6IuDQI3dPTMYqS/WqdzX +0wbxUNb/Fq8kzRD4tR04jvUEkhUQLLOc3ffl+e7tXQoMS60OjA0Y1vfsp7uAqOMh +ofuRSk5uYQT2fayOIq68CDarxWEdtEDKAC5MeHm7poXKOgz+QSuKOepR6b1wuo+B +IODzIJoaT7RGqyg8dKBWMDjRZmztqK1SVagzPPEnxf2OqjxdHWHScpqh6aq74Qiy +zXHHxHFPpG/Ah4SyIB1If3cmQdYUxcK5OpFezD5/++E5U0XKyO13GvgTexyn2Gvu +R1dzMMyEU50xKy/OHLS9Pa2js1lU6k/X4hbOH8F6ADkMVT/d1G8uVQwYZj3oRixQ +ymr1CNbtglAUXb0R5hzlQeA3eAnNWKhK2q3s85x5IBjOj7+zP+hfapIFohogBdoQ +07aRTFi3aEW9xn47JBNCwnR/F69C9We0RNp4it2Kp9mAabTshLxmIc58NrLLeppP +PcQ4bvpNa92s33Seu3m/0baV8iaD8JFZz6aNoiYwEI0CHNJwmehjhgrst4pP4Cc2 +30KljCqSg86hiv9R9OBKL7j1Kip5ZVtP95jrmlf9j+M1XcIbKRLlnTXxA5deRQql +DuUlF9+ioehRqjCvROocePe35Nhu/55em/ksX4FSjnLav5hAIQbC0CZEecmUlpuu +V/bVk1+cGfaqv6QU3G3rfOj2SAAWnfCsOv5wPsUEOAw2e0cpF2AoKYo5LxRJbDmu +VbDX5/euciW6mqlSadXzReUuSLb4wHedfMEw4LzLYmje8I/ukzeFhSrtB7+cbapy +YM+rqrK9zoew/YZtSxNL90dn1xXCmJOvcSm/oJJKg5InEo1e3kYLEq6AqFZCqX9J +YoT2D+iEvoKu2qLy5SWkjzz0aR33hpCqqoeRe2Oaf10w0+D7L/jTtqpJ66qBW/4s +BMtCeUKXc6lX5qpayHXIxdzAGTajHLVi4/Hoic1h0MmRKxzgCt7pAZiuerubPhvY +Uvf5Q4INof8218bEpYvEZNpA7A030SIdymnc+u+J6B+seMFS+Xtvt+0aaFBEvQiy +rYjBkdHRokf3LyHPw4Q53C/2dLx9xuL0b/4JaYGVY2UF +-----END ENCRYPTED PRIVATE KEY----- diff --git a/config/oauth/public.key b/config/oauth/public.key new file mode 100644 index 0000000..493a82e --- /dev/null +++ b/config/oauth/public.key @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxHCTBoDL4DzdqFRDYX1V +lIIVW74IvdN41Yhs2FujdbnDylOh/Iwqa4r20F0RPneKh9lKFEXK5/2hi3I2obRJ +j2AGFNu/lMRt9V05eK5Z3zEmf2RLaI6zLHWxpQQt/NKjcSU9ciO5KaPI5fkwtfvz +v4WLrn+/SWFTM7HkzHBzIekhi+RIiN/iqNy4bsiSbruIDQkCBCAJ+v8vdRzAx/wg +bVfYK4mniP0PzdSzEEGWRyOXShSCj5hFfWAxi1VRor3ORdiNQEtWINFFYsNKBzlx +q7xSQ65Co+ViH2NMo0i5swr8tmXV75AKCvYmuO7MhfSzKwnYiFERnTE2qs2W0NLy +jwIDAQAB +-----END PUBLIC KEY----- diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index 7e1ee1f..b81d06a 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -3,9 +3,10 @@ framework: secret: '%env(APP_SECRET)%' # Note that the session will be started ONLY if you read or write from it. - session: true - - #esi: true + session: + cookie_domain: '.solutions-easy.moi' + cookie_samesite: lax # Use 'none' only with HTTPS + #esi: true #fragments: true when@test: diff --git a/config/packages/league_oauth2_server.yaml b/config/packages/league_oauth2_server.yaml new file mode 100644 index 0000000..ce6d197 --- /dev/null +++ b/config/packages/league_oauth2_server.yaml @@ -0,0 +1,18 @@ +league_oauth2_server: + authorization_server: + private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%' + private_key_passphrase: '%env(resolve:OAUTH_PASSPHRASE)%' + encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%' + resource_server: + public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%' + scopes: + available: ['email'] + default: ['email'] + persistence: + doctrine: + entity_manager: default + +when@test: + league_oauth2_server: + persistence: + in_memory: null diff --git a/config/packages/nyholm_psr7.yaml b/config/packages/nyholm_psr7.yaml new file mode 100644 index 0000000..ade8312 --- /dev/null +++ b/config/packages/nyholm_psr7.yaml @@ -0,0 +1,11 @@ +services: + # Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories) + Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory' + Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory' + + nyholm.psr7.psr17_factory: + class: Nyholm\Psr7\Factory\Psr17Factory diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..5178832 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,14 +4,46 @@ security: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: username firewalls: + api: + pattern: ^/oauth2/api + security: true + stateless: true + oauth2: true + token: + pattern: ^/token + security: false + oauth2_token: + pattern: ^/oauth2/token + security: false dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + authorize: + pattern: ^/authorize + lazy: true + provider: app_user_provider + form_login: + login_path: app_login + check_path: app_login + enable_csrf: true + default_target_path: /authorize main: lazy: true - provider: users_in_memory + provider: app_user_provider + form_login: + login_path: app_login + check_path: app_login + enable_csrf: true + default_target_path: app_login_check + logout: + path: app_logout + target: app_login # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -22,8 +54,15 @@ security: # 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: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/login/check, roles: ROLE_USER } + - { path: ^/authorize, roles: ROLE_USER } + - { path: ^/oauth2/authorize, roles: ROLE_USER } + - { path: ^/oauth2/api, roles: PUBLIC_ACCESS } + - { path: ^/token, roles: PUBLIC_ACCESS } + - { path: ^/oauth2/token, roles: PUBLIC_ACCESS } + - { path: ^/oauth2/userinfo, roles: IS_AUTHENTICATED_FULLY } + - { path: ^/, roles: ROLE_USER } when@test: security: diff --git a/config/routes/league_oauth2_server.yaml b/config/routes/league_oauth2_server.yaml new file mode 100644 index 0000000..b874e00 --- /dev/null +++ b/config/routes/league_oauth2_server.yaml @@ -0,0 +1,3 @@ +oauth2_server: + resource: '@LeagueOAuth2ServerBundle/Resources/config/routes.php' + type: php diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..0fb9afc 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -22,3 +22,16 @@ services: # add more service definitions when explicit configuration is needed # please note that last definitions always *replace* previous ones + + # OAuth2 repositories + App\Repository\ClientRepository: + tags: ['league.oauth2_server.repository'] + + App\Repository\AccessTokenRepository: + tags: ['league.oauth2_server.repository'] + + App\Repository\RefreshTokenRepository: + tags: ['league.oauth2_server.repository'] + + App\Repository\AuthCodeRepository: + tags: ['league.oauth2_server.repository'] diff --git a/migrations/Version20250321144914.php b/migrations/Version20250321144914.php new file mode 100644 index 0000000..6987ca3 --- /dev/null +++ b/migrations/Version20250321144914.php @@ -0,0 +1,48 @@ +addSql('CREATE TABLE "user" (id SERIAL NOT NULL, username VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE TABLE messenger_messages (id BIGSERIAL NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)'); + $this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)'); + $this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)'); + $this->addSql('COMMENT ON COLUMN messenger_messages.created_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.available_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN messenger_messages.delivered_at IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE OR REPLACE FUNCTION notify_messenger_messages() RETURNS TRIGGER AS $$ + BEGIN + PERFORM pg_notify(\'messenger_messages\', NEW.queue_name::text); + RETURN NEW; + END; + $$ LANGUAGE plpgsql;'); + $this->addSql('DROP TRIGGER IF EXISTS notify_trigger ON messenger_messages;'); + $this->addSql('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON messenger_messages FOR EACH ROW EXECUTE PROCEDURE notify_messenger_messages();'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP TABLE "user"'); + $this->addSql('DROP TABLE messenger_messages'); + } +} diff --git a/migrations/Version20250326084945.php b/migrations/Version20250326084945.php new file mode 100644 index 0000000..4c2eba5 --- /dev/null +++ b/migrations/Version20250326084945.php @@ -0,0 +1,31 @@ +addSql('CREATE SCHEMA public'); + } +} diff --git a/src/Command/CreateOAuthClientCommand.php b/src/Command/CreateOAuthClientCommand.php new file mode 100644 index 0000000..d7e025e --- /dev/null +++ b/src/Command/CreateOAuthClientCommand.php @@ -0,0 +1,94 @@ +addArgument('identifier', InputArgument::REQUIRED, 'Client identifier (max 32 chars)') + ->addArgument('name', InputArgument::REQUIRED, 'Client name') + ->addArgument('redirect-uri', InputArgument::REQUIRED, 'Redirect URI') + ->addOption('grant-type', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Grant type (authorization_code, client_credentials, implicit, password, refresh_token)', ['authorization_code']) + ->addOption('scope', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Scope', ['email']) + ->addOption('secret', null, InputOption::VALUE_OPTIONAL, 'Client secret (for confidential clients)') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $identifier = $input->getArgument('identifier'); + $name = $input->getArgument('name'); + $redirectUri = $input->getArgument('redirect-uri'); + $grantTypes = $input->getOption('grant-type'); + $scopes = $input->getOption('scope'); + $secret = $input->getOption('secret'); + + if (strlen($identifier) > 32) { + $io->error('Identifier must be 32 characters or less'); + return Command::FAILURE; + } + + if ($this->clientRepository->find($identifier)) { + $io->error(sprintf('Client with identifier "%s" already exists', $identifier)); + return Command::FAILURE; + } + + $client = new Client($identifier, $secret, $name); + + // Create and spread the redirect URI + $client->setRedirectUris(new RedirectUriVO($redirectUri)); + + // Create the grant type objects + $grantObjects = array_map(fn($grantType) => new GrantVO($grantType), $grantTypes); + // Spread the grant objects to the setter + $client->setGrants(...$grantObjects); + + // Create the scope objects + $scopeObjects = array_map(fn($scope) => new ScopeVO($scope), $scopes); + // Spread the scope objects to the setter + $client->setScopes(...$scopeObjects); + + $this->clientRepository->save($client, true); + + $io->success(sprintf('Client "%s" created successfully', $identifier)); + $io->table( + ['Property', 'Value'], + [ + ['Identifier', $identifier], + ['Secret', $secret ?: 'none (public client)'], + ['Name', $name], + ['Redirect URI', $redirectUri], + ['Grant Types', implode(', ', $grantTypes)], + ['Scopes', implode(', ', $scopes)], + ] + ); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php new file mode 100644 index 0000000..eb8b03a --- /dev/null +++ b/src/Command/CreateUserCommand.php @@ -0,0 +1,88 @@ +addArgument('username', InputArgument::REQUIRED, 'Username') + ->addArgument('email', InputArgument::REQUIRED, 'Email address') + ->addArgument('password', InputArgument::REQUIRED, 'Password') + ->addOption('admin', null, InputOption::VALUE_NONE, 'Set as admin user') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $username = $input->getArgument('username'); + $email = $input->getArgument('email'); + $plainPassword = $input->getArgument('password'); + $isAdmin = $input->getOption('admin'); + + // Check if username already exists + $existingUser = $this->userRepository->findOneBy(['username' => $username]); + if ($existingUser) { + $io->error(sprintf('User with username "%s" already exists', $username)); + return Command::FAILURE; + } + + // Check if email already exists + $existingUser = $this->userRepository->findOneBy(['email' => $email]); + if ($existingUser) { + $io->error(sprintf('User with email "%s" already exists', $email)); + return Command::FAILURE; + } + + $user = new User(); + $user->setUsername($username); + $user->setEmail($email); + + $hashedPassword = $this->passwordHasher->hashPassword($user, $plainPassword); + $user->setPassword($hashedPassword); + + $roles = ['ROLE_USER']; + if ($isAdmin) { + $roles[] = 'ROLE_ADMIN'; + } + $user->setRoles($roles); + + $this->userRepository->save($user, true); + + $io->success(sprintf('User "%s" created successfully', $username)); + $io->table( + ['Property', 'Value'], + [ + ['Username', $username], + ['Email', $email], + ['Roles', implode(', ', $user->getRoles())], + ] + ); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/src/Controller/OAuth2Controller.php b/src/Controller/OAuth2Controller.php new file mode 100644 index 0000000..9501ce1 --- /dev/null +++ b/src/Controller/OAuth2Controller.php @@ -0,0 +1,35 @@ +getUser()) { + // Store the original request parameters + $session = $request->getSession(); + $session->set('oauth_authorization_request', $request->query->all()); + + // Redirect to login + return $this->redirectToRoute('app_login'); + } + + // User is logged in, let the OAuth2 server handle the authorization + // We'll just forward the request to the League OAuth2 Server bundle's controller + return $this->forward('league.oauth2_server.controller.authorization::indexAction'); + } +} \ No newline at end of file diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..6749c2a --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,85 @@ +getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + + #[Route('/logout', name: 'app_logout')] + public function logout(): void + { + // This method can be empty - it will be intercepted by the logout key on your firewall + // The logout is handled by Symfony's security system + } + + #[Route('/', name: 'app_home')] + public function home(): Response + { + return $this->render('security/home.html.twig', [ + 'user' => $this->getUser(), + ]); + } + + #[Route('/connect/sso', name: 'connect_sso_start')] + public function connectAction(ClientRegistry $clientRegistry): Response + { + return $clientRegistry + ->getClient('sso_server') + ->redirect([ + 'profile', + 'email' + ]); + } + + #[Route('/connect/sso/check', name: 'connect_sso_check')] + public function connectCheckAction(): Response + { + // This method can be empty - it will be intercepted by the oauth2 firewall + throw new \Exception('Don\'t forget to activate check_path in security.yaml'); + } + + /** + * This route is used after successful authentication to redirect + * back to the OAuth2 authorization if there was a pending request + */ + #[Route('/login/check', name: 'app_login_check')] + public function loginCheck(Request $request): Response + { + // Check if there was a pending OAuth2 authorization request + $session = $request->getSession(); + $oauthRequest = $session->get('oauth_authorization_request'); + + if ($oauthRequest) { + // Clear the stored request + $session->remove('oauth_authorization_request'); + + // Rebuild the authorization URL + $queryString = http_build_query($oauthRequest); + return $this->redirect('/authorize?' . $queryString); + } + + // No pending OAuth2 request, go to homepage + return $this->redirectToRoute('app_home'); + } +} diff --git a/src/Controller/UserInfoController.php b/src/Controller/UserInfoController.php new file mode 100644 index 0000000..64f7991 --- /dev/null +++ b/src/Controller/UserInfoController.php @@ -0,0 +1,28 @@ +getUser(); + + if (!$user) { + return new JsonResponse(['error' => 'Unauthorized'], 401); + } + + return new JsonResponse([ + 'sub' => $user->getId(), + 'username' => $user->getUsername(), + 'email' => $user->getEmail(), + 'roles' => $user->getRoles(), + ]); + } +} \ No newline at end of file diff --git a/src/Entity/AccessToken.php b/src/Entity/AccessToken.php new file mode 100644 index 0000000..e868b66 --- /dev/null +++ b/src/Entity/AccessToken.php @@ -0,0 +1,111 @@ +identifier = $identifier; + $this->expiry = $expiry; + $this->userIdentifier = $userIdentifier; + $this->clientIdentifier = $clientIdentifier; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getExpiry(): \DateTimeImmutable + { + return $this->expiry; + } + + /** + * Proxies to getExpiry for backward compatibility with older versions of the library + */ + public function getExpiryDateTime(): \DateTimeImmutable + { + return $this->getExpiry(); + } + + public function getUserIdentifier(): string + { + return $this->userIdentifier; + } + + public function getClient(): ClientInterface + { + throw new \RuntimeException('Method not implemented.'); + } + + public function getClientIdentifier(): string + { + return $this->clientIdentifier; + } + + public function isRevoked(): bool + { + return $this->revoked; + } + + public function revoke(): AccessTokenInterface + { + $this->revoked = true; + return $this; + } + + public function getScopes(): array + { + return array_map( + static fn (string $scope): Scope => new ScopeVO($scope), + $this->scopes + ); + } + + public function setScopes(array $scopes): self + { + $this->scopes = array_map( + static fn (Scope $scope): string => (string) $scope, + $scopes + ); + return $this; + } + + public function __toString(): string + { + return $this->getIdentifier(); + } +} \ No newline at end of file diff --git a/src/Entity/AuthCode.php b/src/Entity/AuthCode.php new file mode 100644 index 0000000..01fdafc --- /dev/null +++ b/src/Entity/AuthCode.php @@ -0,0 +1,121 @@ +identifier = $identifier; + $this->expiry = $expiry; + $this->userIdentifier = $userIdentifier; + $this->clientIdentifier = $clientIdentifier; + $this->redirectUri = $redirectUri; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getExpiry(): \DateTimeImmutable + { + return $this->expiry; + } + + /** + * Proxies to getExpiry for backward compatibility with older versions of the library + */ + public function getExpiryDateTime(): \DateTimeImmutable + { + return $this->getExpiry(); + } + + public function getUserIdentifier(): ?string + { + return $this->userIdentifier; + } + + public function getClient(): ClientInterface + { + throw new \RuntimeException('Method not implemented.'); + } + + public function getClientIdentifier(): string + { + return $this->clientIdentifier; + } + + public function getRedirectUri(): ?string + { + return $this->redirectUri; + } + + public function isRevoked(): bool + { + return $this->revoked; + } + + public function revoke(): AuthorizationCodeInterface + { + $this->revoked = true; + return $this; + } + + public function getScopes(): array + { + return array_map( + static fn (string $scope): Scope => new ScopeVO($scope), + $this->scopes + ); + } + + public function setScopes(array $scopes): self + { + $this->scopes = array_map( + static fn (Scope $scope): string => (string) $scope, + $scopes + ); + return $this; + } + + public function __toString(): string + { + return $this->getIdentifier(); + } +} \ No newline at end of file diff --git a/src/Entity/Client.php b/src/Entity/Client.php new file mode 100644 index 0000000..76cff78 --- /dev/null +++ b/src/Entity/Client.php @@ -0,0 +1,160 @@ + false])] + private bool $allowPlainTextPkce = false; + + public function __construct(string $identifier, ?string $secret, string $name) + { + $this->identifier = $identifier; + $this->secret = $secret; + $this->name = $name; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getSecret(): ?string + { + return $this->secret; + } + + public function getName(): string + { + return $this->name; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): self + { + $this->active = $active; + return $this; + } + + public function getRedirectUri(): string|array + { + $uris = array_map( + static fn (string $redirectUri): string => $redirectUri, + $this->redirectUris + ); + + return $uris; + } + + public function getGrants(): array + { + return array_map( + static fn (string $grant): GrantVO => new GrantVO($grant), + $this->grants + ); + } + + public function setGrants(GrantVO ...$grants): self + { + $this->grants = array_map( + static fn (GrantVO $grant): string => (string) $grant, + $grants + ); + return $this; + } + + public function getScopes(): array + { + return array_map( + static fn (string $scope): ScopeVO => new ScopeVO($scope), + $this->scopes + ); + } + + public function setScopes(ScopeVO ...$scopes): self + { + $this->scopes = array_map( + static fn (ScopeVO $scope): string => (string) $scope, + $scopes + ); + return $this; + } + + public function getMetadata(): ClientMetadata + { + return new ClientMetadata($this->name); + } + + public function isConfidential(): bool + { + return $this->secret !== null; + } + + public function isPlainTextPkceAllowed(): bool + { + return $this->allowPlainTextPkce; + } + + public function setAllowPlainTextPkce(bool $allowPlainTextPkce): self + { + $this->allowPlainTextPkce = $allowPlainTextPkce; + return $this; + } + + public function __toString(): string + { + return $this->getIdentifier(); + } + + public function getRedirectUris(): array + { + return array_map( + static fn (string $redirectUri): RedirectUriVO => new RedirectUriVO($redirectUri), + $this->redirectUris + ); + } + + public function setRedirectUris(RedirectUriVO ...$redirectUris): self + { + $this->redirectUris = array_map( + static fn (RedirectUriVO $redirectUri): string => (string) $redirectUri, + $redirectUris + ); + return $this; + } +} \ No newline at end of file diff --git a/src/Entity/RefreshToken.php b/src/Entity/RefreshToken.php new file mode 100644 index 0000000..f736828 --- /dev/null +++ b/src/Entity/RefreshToken.php @@ -0,0 +1,79 @@ +identifier = $identifier; + $this->expiry = $expiry; + $this->accessTokenIdentifier = $accessTokenIdentifier; + } + + public function getIdentifier(): string + { + return $this->identifier; + } + + public function getExpiry(): \DateTimeImmutable + { + return $this->expiry; + } + + /** + * Proxies to getExpiry for backward compatibility with older versions of the library + */ + public function getExpiryDateTime(): \DateTimeImmutable + { + return $this->getExpiry(); + } + + public function getAccessToken(): AccessTokenInterface + { + throw new \RuntimeException('Method not implemented.'); + } + + public function getAccessTokenIdentifier(): string + { + return $this->accessTokenIdentifier; + } + + public function isRevoked(): bool + { + return $this->revoked; + } + + public function revoke(): RefreshTokenInterface + { + $this->revoked = true; + return $this; + } + + public function __toString(): string + { + return $this->getIdentifier(); + } +} \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..27b05ea --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,120 @@ +id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): static + { + $this->username = $username; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->username; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + /** + * Returns a string that can be used as a user identifier for the OAuth2 server. + */ + public function getOAuth2Identifier(): string + { + return (string) $this->getId(); + } +} diff --git a/src/Repository/AccessTokenRepository.php b/src/Repository/AccessTokenRepository.php new file mode 100644 index 0000000..e0ffe17 --- /dev/null +++ b/src/Repository/AccessTokenRepository.php @@ -0,0 +1,96 @@ + + */ +class AccessTokenRepository extends ServiceEntityRepository implements AccessTokenRepositoryInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AccessToken::class); + } + + public function save(AccessToken $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(AccessToken $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getAccessToken(string $identifier): ?AccessTokenInterface + { + return $this->find($identifier); + } + + public function persistNewAccessToken(AccessTokenInterface $accessToken): void + { + if (!$accessToken instanceof AccessToken) { + throw new \RuntimeException('Invalid access token type'); + } + + $this->save($accessToken, true); + } + + public function revokeAccessToken(string $identifier): void + { + $accessToken = $this->find($identifier); + if (null !== $accessToken) { + $accessToken->revoke(); + $this->save($accessToken, true); + } + } + + public function isAccessTokenRevoked(string $identifier): bool + { + $accessToken = $this->find($identifier); + if (null === $accessToken) { + return true; + } + + return $accessToken->isRevoked(); + } + + public function createNewAccessToken( + ClientInterface $client, + array $scopes, + string $userIdentifier = null + ): AccessTokenInterface { + $accessToken = new AccessToken( + bin2hex(random_bytes(40)), + new \DateTimeImmutable('+1 hour'), + $userIdentifier ?? '', + $client->getIdentifier() + ); + + $accessToken->setScopes(array_map( + static fn(string $scope) => new Scope($scope), + array_map( + static fn($scope) => (string) $scope, + $scopes + ) + )); + + return $accessToken; + } +} \ No newline at end of file diff --git a/src/Repository/AuthCodeRepository.php b/src/Repository/AuthCodeRepository.php new file mode 100644 index 0000000..dc7e9f2 --- /dev/null +++ b/src/Repository/AuthCodeRepository.php @@ -0,0 +1,98 @@ + + */ +class AuthCodeRepository extends ServiceEntityRepository implements AuthorizationCodeRepositoryInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, AuthCode::class); + } + + public function save(AuthCode $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(AuthCode $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getAuthorizationCode(string $identifier): ?AuthorizationCodeInterface + { + return $this->find($identifier); + } + + public function persistNewAuthorizationCode(AuthorizationCodeInterface $authorizationCode): void + { + if (!$authorizationCode instanceof AuthCode) { + throw new \RuntimeException('Invalid authorization code type'); + } + + $this->save($authorizationCode, true); + } + + public function revokeAuthorizationCode(string $identifier): void + { + $authCode = $this->find($identifier); + if (null !== $authCode) { + $authCode->revoke(); + $this->save($authCode, true); + } + } + + public function isAuthorizationCodeRevoked(string $identifier): bool + { + $authCode = $this->find($identifier); + if (null === $authCode) { + return true; + } + + return $authCode->isRevoked(); + } + + public function createNewAuthorizationCode( + ClientInterface $client, + array $scopes, + ?string $userIdentifier, + ?string $redirectUri + ): AuthorizationCodeInterface { + $authCode = new AuthCode( + bin2hex(random_bytes(40)), + new \DateTimeImmutable('+5 minutes'), + $userIdentifier ?? '', + $client->getIdentifier(), + $redirectUri + ); + + $authCode->setScopes(array_map( + static fn(string $scope) => new Scope($scope), + array_map( + static fn($scope) => (string) $scope, + $scopes + ) + )); + + return $authCode; + } +} \ No newline at end of file diff --git a/src/Repository/ClientRepository.php b/src/Repository/ClientRepository.php new file mode 100644 index 0000000..f241f3f --- /dev/null +++ b/src/Repository/ClientRepository.php @@ -0,0 +1,75 @@ + + */ +class ClientRepository extends ServiceEntityRepository implements ClientRepositoryInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, Client::class); + } + + public function save(Client $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(Client $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface + { + return $this->find($clientIdentifier); + } + + public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool + { + $client = $this->find($clientIdentifier); + + if (null === $client || !$client->isActive()) { + return false; + } + + // Validate grant type + if (null !== $grantType) { + $validGrantTypes = array_map( + static fn($grant) => (string) $grant, + $client->getGrants() + ); + + if (!in_array($grantType, $validGrantTypes, true)) { + return false; + } + } + + // Validate secret + if (null === $client->getSecret()) { + return true; + } + + if (null === $clientSecret) { + return false; + } + + return $client->getSecret() === $clientSecret; + } +} \ No newline at end of file diff --git a/src/Repository/RefreshTokenRepository.php b/src/Repository/RefreshTokenRepository.php new file mode 100644 index 0000000..80aa787 --- /dev/null +++ b/src/Repository/RefreshTokenRepository.php @@ -0,0 +1,83 @@ + + */ +class RefreshTokenRepository extends ServiceEntityRepository implements RefreshTokenRepositoryInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, RefreshToken::class); + } + + public function save(RefreshToken $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(RefreshToken $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function getRefreshToken(string $identifier): ?RefreshTokenInterface + { + return $this->find($identifier); + } + + public function persistNewRefreshToken(RefreshTokenInterface $refreshToken): void + { + if (!$refreshToken instanceof RefreshToken) { + throw new \RuntimeException('Invalid refresh token type'); + } + + $this->save($refreshToken, true); + } + + public function revokeRefreshToken(string $identifier): void + { + $refreshToken = $this->find($identifier); + if (null !== $refreshToken) { + $refreshToken->revoke(); + $this->save($refreshToken, true); + } + } + + public function isRefreshTokenRevoked(string $identifier): bool + { + $refreshToken = $this->find($identifier); + if (null === $refreshToken) { + return true; + } + + return $refreshToken->isRevoked(); + } + + public function createNewRefreshToken( + AccessTokenInterface $accessToken, + \DateTimeImmutable $expiry + ): RefreshTokenInterface { + return new RefreshToken( + bin2hex(random_bytes(40)), + $expiry, + $accessToken->getIdentifier() + ); + } +} \ No newline at end of file diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..412aadc --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,74 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + public function findByUsername(string $username): ?User + { + return $this->findOneBy(['username' => $username]); + } + + public function save(User $user, bool $flush = false): void + { + $this->getEntityManager()->persist($user); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/symfony.lock b/symfony.lock index 4947424..e97c107 100644 --- a/symfony.lock +++ b/symfony.lock @@ -26,6 +26,31 @@ "migrations/.gitignore" ] }, + "league/oauth2-server-bundle": { + "version": "0.10", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "0.4", + "ref": "7f26963037fce3b69c1b3f6a75a3265edb1e1caa" + }, + "files": [ + "config/packages/league_oauth2_server.yaml", + "config/routes/league_oauth2_server.yaml" + ] + }, + "nyholm/psr7": { + "version": "1.8", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "1.0", + "ref": "4a8c0345442dcca1d8a2c65633dcf0285dd5a5a2" + }, + "files": [ + "config/packages/nyholm_psr7.yaml" + ] + }, "phpunit/phpunit": { "version": "9.6", "recipe": { diff --git a/templates/base.html.twig b/templates/base.html.twig index 3cda30f..f24cbb6 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -2,13 +2,16 @@
+/home/margueritecharles-edouard/Workspace/serveurSSO/src/Controller/SecurityController.php/home/margueritecharles-edouard/Workspace/serveurSSO/templates/security/index.html.twig