From 1cf3044980c4e5aa09e1645dd6a5926099e34a05 Mon Sep 17 00:00:00 2001 From: yena Date: Thu, 28 Mar 2024 18:11:24 +0100 Subject: [PATCH] [WIP] login api Adds LexikJWT auth bundle and JSON login TODO: Add tests --- .env | 6 + .gitignore | 4 + composer.json | 1 + composer.lock | 325 +++++++++++++++++- config/bundles.php | 1 + config/packages/lexik_jwt_authentication.yaml | 14 + config/packages/security.yaml | 25 ++ src/Controller/ApiLoginController.php | 30 ++ .../JWTAuthenticationSuccessHandler.php | 20 ++ .../TwoFactorAuthenticationFailureHandler.php | 16 + ...TwoFactorAuthenticationRequiredHandler.php | 16 + .../TwoFactorAuthenticationSuccessHandler.php | 16 + symfony.lock | 12 + 13 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 config/packages/lexik_jwt_authentication.yaml create mode 100644 src/Controller/ApiLoginController.php create mode 100644 src/Security/JWTAuthenticationSuccessHandler.php create mode 100644 src/Security/TwoFactorAuthenticationFailureHandler.php create mode 100644 src/Security/TwoFactorAuthenticationRequiredHandler.php create mode 100644 src/Security/TwoFactorAuthenticationSuccessHandler.php diff --git a/.env b/.env index 50f0129f..a00827d5 100644 --- a/.env +++ b/.env @@ -54,3 +54,9 @@ MAILER_DELIVERY_ADDRESS="admin@example.org" # DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=14&charset=utf8" DATABASE_URL="mysql://mail:password@127.0.0.1:3306/mail?charset=utf8mb4" ###< 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=857ce6bfb8d058ca8f51421d40864305d96be4be4a30d75e843b2d13958e6fde +###< lexik/jwt-authentication-bundle ### diff --git a/.gitignore b/.gitignore index ebd16754..d8e13a54 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,7 @@ translations/ ###> phpstan/phpstan ### phpstan.neon ###< phpstan/phpstan ### + +###> lexik/jwt-authentication-bundle ### +/config/jwt/*.pem +###< lexik/jwt-authentication-bundle ### diff --git a/composer.json b/composer.json index cd991b86..6e660bf5 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "endroid/qr-code": "^5.0", "ircmaxell/password-compat": "~1.0.3", "knplabs/knp-menu-bundle": "^3.0", + "lexik/jwt-authentication-bundle": "^2.20", "mopa/bootstrap-bundle": "~3.0", "nelmio/security-bundle": "^3.0", "pear/crypt_gpg": "^1.6", diff --git a/composer.lock b/composer.lock index cc20dcab..4b679e1b 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": "07e591e20b64786dbf717457f756a368", + "content-hash": "c9706f16061b30b51ae159d4e070268e", "packages": [ { "name": "bacon/bacon-qr-code", @@ -2105,6 +2105,262 @@ }, "time": "2023-11-01T09:25:40+00:00" }, + { + "name": "lcobucci/clock", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/6f28b826ea01306b07980cb8320ab30b966cd715", + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.27", + "lcobucci/coding-standard": "^11.0.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": "^10.2.3" + }, + "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.2.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2023-11-17T17:00:27+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.2.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/0ba88aed12c04bd2ed9924f500673f32b67a6211", + "reference": "0ba88aed12c04bd2ed9924f500673f32b67a6211", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.27.0", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2.9", + "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.2.6" + }, + "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.2.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2023-11-20T21:17:42+00:00" + }, + { + "name": "lexik/jwt-authentication-bundle", + "version": "v2.20.3", + "source": { + "type": "git", + "url": "https://github.com/lexik/LexikJWTAuthenticationBundle.git", + "reference": "a196d68d07dd5486a523cc3415620badbb5d25c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lexik/LexikJWTAuthenticationBundle/zipball/a196d68d07dd5486a523cc3415620badbb5d25c2", + "reference": "a196d68d07dd5486a523cc3415620badbb5d25c2", + "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.4|^6.0|^7.0", + "symfony/dependency-injection": "^4.4|^5.4|^6.0|^7.0", + "symfony/deprecation-contracts": "^2.4|^3.0", + "symfony/event-dispatcher": "^4.4|^5.4|^6.0|^7.0", + "symfony/http-foundation": "^4.4|^5.4|^6.0|^7.0", + "symfony/http-kernel": "^4.4|^5.4|^6.0|^7.0", + "symfony/property-access": "^4.4|^5.4|^6.0|^7.0", + "symfony/security-bundle": "^4.4|^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^1.0|^2.0|^3.0" + }, + "conflict": { + "symfony/console": "<4.4" + }, + "require-dev": { + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/filesystem": "^4.4|^5.4|^6.0|^7.0", + "symfony/framework-bundle": "^4.4|^5.4|^6.0|^7.0", + "symfony/phpunit-bridge": "^4.4|^5.4|^6.0|^7.0", + "symfony/security-guard": "^4.4|^5.4|^6.0|^7.0", + "symfony/var-dumper": "^4.4|^5.4|^6.0|^7.0", + "symfony/yaml": "^4.4|^5.4|^6.0|^7.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.20.3" + }, + "funding": [ + { + "url": "https://github.com/chalasr", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/lexik/jwt-authentication-bundle", + "type": "tidelift" + } + ], + "time": "2023-12-14T15:58:11+00:00" + }, { "name": "monolog/monolog", "version": "3.5.0", @@ -2338,6 +2594,73 @@ }, "time": "2022-12-29T11:23:59+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": "nelmio/security-bundle", "version": "v3.3.0", diff --git a/config/bundles.php b/config/bundles.php index bb507420..79937ed5 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -21,4 +21,5 @@ Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], FriendsOfBehat\SymfonyExtension\Bundle\FriendsOfBehatSymfonyExtensionBundle::class => ['test' => 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 00000000..66c59330 --- /dev/null +++ b/config/packages/lexik_jwt_authentication.yaml @@ -0,0 +1,14 @@ +lexik_jwt_authentication: + secret_key: "%env(resolve:JWT_SECRET_KEY)%" + public_key: "%env(resolve:JWT_PUBLIC_KEY)%" + pass_phrase: "%env(JWT_PASSPHRASE)%" + token_ttl: 3600 + token_extractors: + authorization_header: + enabled: true + prefix: Bearer + name: Authorization + +when@dev: + lexik_jwt_authentication: + token_ttl: 31536000 diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 84a3c2dc..28add15a 100755 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -104,6 +104,28 @@ security: dev: pattern: ^/(_(profiler|error|wdt)|css|images|js)/ security: false + api-login: + pattern: ^/api/login + stateless: false + json_login: + check_path: api_login + username_path: data.username + password_path: data.password + success_handler: App\Security\JWTAuthenticationSuccessHandler + failure_handler: lexik_jwt_authentication.handler.authentication_failure + two_factor: + check_path: api_login_2fa + prepare_on_login: true + prepare_on_access_denied: true + post_only: true + auth_code_parameter_name: data.totp + authentication_required_handler: App\Security\TwoFactorAuthenticationRequiredHandler + success_handler: App\Security\TwoFactorAuthenticationSuccessHandler + failure_handler: App\Security\TwoFactorAuthenticationFailureHandler + api: + pattern: ^/api + stateless: false + jwt: ~ main: pattern: ^/ provider: user @@ -129,6 +151,7 @@ security: # 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: @@ -144,3 +167,5 @@ security: - { path: "^/alias", roles: ROLE_USER, allow_if: "!is_granted('ROLE_SPAM')"} - { path: "^/account", roles: ROLE_USER, allow_if: "!is_granted('ROLE_SPAM')"} - { path: "^/admin", roles: ROLE_DOMAIN_ADMIN } + - { path: "^/api/login", roles: PUBLIC_ACCESS } + - { path: "^/api", roles: ROLE_USER } \ No newline at end of file diff --git a/src/Controller/ApiLoginController.php b/src/Controller/ApiLoginController.php new file mode 100644 index 00000000..9ba502da --- /dev/null +++ b/src/Controller/ApiLoginController.php @@ -0,0 +1,30 @@ +createAccessDeniedException("User not in 2fa process"); + $jsonResponse = new Response(json_encode($error), Response::HTTP_BAD_REQUEST); + $jsonResponse->headers->set('Content-Type', 'application/json'); + return $jsonResponse; + } + + } +} diff --git a/src/Security/JWTAuthenticationSuccessHandler.php b/src/Security/JWTAuthenticationSuccessHandler.php new file mode 100644 index 00000000..aa2e9659 --- /dev/null +++ b/src/Security/JWTAuthenticationSuccessHandler.php @@ -0,0 +1,20 @@ +handleAuthenticationSuccess($token->getUser()); + } +} \ No newline at end of file diff --git a/src/Security/TwoFactorAuthenticationFailureHandler.php b/src/Security/TwoFactorAuthenticationFailureHandler.php new file mode 100644 index 00000000..b2051e70 --- /dev/null +++ b/src/Security/TwoFactorAuthenticationFailureHandler.php @@ -0,0 +1,16 @@ +handleAuthenticationSuccess($token->getUser()); + } +} diff --git a/symfony.lock b/symfony.lock index b828ced1..7d0696d2 100644 --- a/symfony.lock +++ b/symfony.lock @@ -125,6 +125,18 @@ "knplabs/knp-menu-bundle": { "version": "v2.2.1" }, + "lexik/jwt-authentication-bundle": { + "version": "2.20", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "2.5", + "ref": "e9481b233a11ef7e15fe055a2b21fd3ac1aa2bb7" + }, + "files": [ + "config/packages/lexik_jwt_authentication.yaml" + ] + }, "monolog/monolog": { "version": "1.23.0" },