Skip to content

Commit

Permalink
5.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
atlance committed Nov 28, 2023
1 parent 263c043 commit de577da
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 163 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/php-analyze.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: ['7.4']
php-version: ['7.4', '8.3']

steps:
- uses: actions/checkout@master
Expand Down
57 changes: 26 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
Symfony 4: JWT Authentication
Symfony 5: JWT Authentication
==============
[![composer.lock](http://poser.pugx.org/phpunit/phpunit/composerlock)](https://packagist.org/packages/phpunit/phpunit)
[![PHP analyze & tests](https://github.com/atlance/jwt-auth/actions/workflows/php-analyze.yml/badge.svg)](https://github.com/atlance/jwt-auth/actions/workflows/php-analyze.yml)
![Psalm coverage](https://shepherd.dev/github/atlance/jwt-auth/coverage.svg)
![GitHub](https://img.shields.io/badge/PHPStan-level%208-brightgreen.svg?style=flat)
![GitHub](https://img.shields.io/badge/PHPStan-level%20max-brightgreen.svg?style=flat)
[![codecov](https://codecov.io/gh/atlance/jwt-auth/graph/badge.svg?token=EV9EVMTRTL)](https://codecov.io/gh/atlance/jwt-auth)

## Installation

1. <a href="/docs/generate_keys.md" target="_blank">Generate</a> keys.
2. Install package via composer: `composer require atlance/jwt-auth ^4.0`.
2. Install package via composer: `composer require atlance/jwt-auth ^5.0`.
3. Configure:
- Copy/paste <a href="/src/Resources/config/atlance_jwt_auth.yaml" target="_blank">configuration</a> to
`config/packages/atlance_jwt_auth.yaml`.
Expand All @@ -18,7 +18,7 @@ Symfony 4: JWT Authentication
## Use Case

### Create:
- **Implemented:** `Atlance\JwtAuth\Security\UseCase\Create\Token\Handler`.
- **Implemened:** `Atlance\JwtAuth\Security\UseCase\Create\Token\Handler`.
- **Example**:
```php
<?php
Expand All @@ -28,60 +28,57 @@ declare(strict_types=1);
namespace App\Controller\Login;

use Atlance\JwtAuth\Security\UseCase;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

/**
* @IsGranted("IS_AUTHENTICATED_ANONYMOUSLY")
* @Route("/login", name=Controller::class, methods={"POST"})
*/
#[Route('/login', name: self::class, methods: ['POST'])]
final class Controller extends AbstractController
{
public function __invoke(
Request $request,
UserProviderInterface $provider,
UserPasswordEncoderInterface $encoder,
UseCase\Create\Token\HandlerInterface $handler
UserPasswordHasherInterface $hasher,
UseCase\Create\Token\HandlerInterface $handler,
): JsonResponse {
/** @var array{username:string,password:string} $dataset */
$dataset = json_decode((string) $request->getContent(), true);
$dataset = json_decode($request->getContent(), true);

try {
$user = $provider->loadUserByUsername($dataset['username']);
$encoder->isPasswordValid($user, $encoder->encodePassword($user, $dataset['password']));
$user = $provider->loadUserByIdentifier($dataset['username']);
$hasher->isPasswordValid($user, $hasher->hashPassword($user, $dataset['password']));

return new JsonResponse(['token' => $handler->handle($user)]);
} catch (UsernameNotFoundException $e) {
return new JsonResponse(null, Response::HTTP_BAD_REQUEST);
} catch (UserNotFoundException) {
return new JsonResponse(status: Response::HTTP_BAD_REQUEST);
}
}
}
```

### Access:
**Implemented:** `Atlance\JwtAuth\Security\UseCase\Access\Token\Handler`.
**Implemened:** `Atlance\JwtAuth\Security\UseCase\Access\Token\Handler`.

1) Create custom [JWT Authenticator](./tests/Kernel/Infrastructure/Http/Security/Authentication/JWTAuthenticator.php).
2) Configure `security firewall`:

confugure
```yaml
# config/packages/security.yaml
security:
enable_authenticator_manager: true
firewalls:
main:
guard:
authenticators:
main:
custom_authenticators:
- App\Infrastructure\Http\Security\Authentication\JWTAuthenticator
```
- **And Symfony automatically used JWT for authentication**.
- **More:** <a href="https://symfony.com/doc/4.4/security/guard_authentication.html" target="_blank">How to use
Access Token Authentication</a>.
- **More:** <a href="https://symfony.com/doc/5.4/security/custom_authenticator.html" target="_blank">How to use Access Token Authentication</a>.
- **Example**:
```php
<?php
Expand All @@ -95,20 +92,18 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

/**
* @IsGranted("ROLE_USER")
* @Route("/profile", name=Controller::class, methods={"GET"})
*/
#[IsGranted('ROLE_USER')]
#[Route('/profile', name: self::class, methods: ['GET'])]
final class Controller extends AbstractController
{
public function __invoke(): JsonResponse
{
return new JsonResponse(['username' => $this->getUser()->getUsername()]);
return new JsonResponse(['username' => $this->getUser()->getUserIdentifier()]);
}
}
```

Resources
---------
* [component symfony/security](https://github.com/symfony/security-bundle/tree/4.4)
* [decorator of lcobucci/jwt](https://github.com/atlance/jwt-core/1.0.0)
* [component symfony/security](https://github.com/symfony/security-bundle/tree/5.4)
* [decorator of lcobucci/jwt](https://github.com/atlance/jwt-core)
26 changes: 12 additions & 14 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,34 +10,32 @@
}
],
"require": {
"php": "^7.4",
"atlance/jwt-core": "^0.1",
"symfony/security-bundle": "^4.0",
"symfony/yaml": "^4.0"
"php": "^7.4 || ^8.0",
"atlance/jwt-core": "^0.2",
"symfony/security-bundle": "^5.4",
"symfony/yaml": "^5.4"
},
"require-dev": {
"ext-json": "*",
"ext-mbstring": "*",
"brianium/paratest": "^6.0",
"doctrine/annotations": "^1.0",
"doctrine/annotations": "^2.0",
"ergebnis/composer-normalize": "^2.0",
"fakerphp/faker": "^1.0",
"friendsofphp/php-cs-fixer": "^3.0",
"overtrue/phplint": "^3.0",
"overtrue/phplint": "^3.4 || ^4.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-strict-rules": "^1.0",
"phpstan/phpstan-symfony": "^1.0",
"psalm/plugin-symfony": "^4.0",
"rector/rector": "^0.18.11",
"psalm/plugin-symfony": "^5.0",
"rector/rector": "^0.18",
"sensio/framework-extra-bundle": "^6.0",
"squizlabs/php_codesniffer": "^3.0",
"symfony/browser-kit": "^4.0",
"symfony/framework-bundle": "^4.0",
"symfony/validator": "^4.0",
"vimeo/psalm": "^4.0"
"symfony/browser-kit": "^5.4",
"symfony/framework-bundle": "^5.4",
"symfony/validator": "^5.4",
"vimeo/psalm": "^5.16"
},
"minimum-stability": "stable",
"autoload": {
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
/**
* @psalm-suppress MixedMethodCall
* @psalm-suppress PossiblyUndefinedMethod
* @psalm-suppress UndefinedMethod
*/
class Configuration implements ConfigurationInterface
{
Expand Down
68 changes: 39 additions & 29 deletions src/Resources/config/services.yaml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
parameters:
# configured in extension load.
# configured in extension load.
services:
_defaults:
autowire: true
autoconfigure: true
public: false

# > --- lcobucci/jwt ---
Lcobucci\Clock\Clock:
factory: [ 'Lcobucci\Clock\SystemClock', 'fromSystemTimezone' ]

# > --- lcobucci/jwt ---
Lcobucci\JWT\Decoder: # Interface
class: Lcobucci\JWT\Parsing\Decoder
class: Lcobucci\JWT\Encoding\JoseEncoder

Lcobucci\JWT\Encoder: # Interface
class: Lcobucci\JWT\Parsing\Encoder
class: Lcobucci\JWT\Encoding\JoseEncoder

Lcobucci\JWT\Signer: # Interface
factory: [ 'Atlance\JwtCore\Token\Factory\SignerResolver', 'resolve' ]
Expand All @@ -30,8 +30,8 @@ services:
factory: [ 'Lcobucci\JWT\Signer\Key\InMemory', 'file' ]
arguments: [ '%atlance_jwt_auth.private_key%', '%atlance_jwt_auth.private_passphrase%' ]

Lcobucci\JWT\Parser: # Class
class: Lcobucci\JWT\Parser
Lcobucci\JWT\Parser: # Interface
class: Lcobucci\JWT\Token\Parser
arguments: [ '@Lcobucci\JWT\Decoder' ]

Lcobucci\JWT\ClaimsFormatter: # Interface
Expand All @@ -40,50 +40,61 @@ services:
Lcobucci\JWT\Validator: # Interface
class: Lcobucci\JWT\Validation\Validator

Lcobucci\JWT\Builder: # Class
class: Lcobucci\JWT\Builder
Lcobucci\JWT\Builder: # Interface
class: Lcobucci\JWT\Token\Builder
arguments: [ '@Lcobucci\JWT\Encoder', '@Lcobucci\JWT\ClaimsFormatter' ]

Lcobucci\JWT\Validation\SignedWith: # Class
Lcobucci\JWT\Validation\SignedWith: # Interface
class: Lcobucci\JWT\Validation\Constraint\SignedWith
arguments: [ '@Lcobucci\JWT\Signer', '@jwt_auth.private_key' ]

Lcobucci\JWT\Validation\ValidAt: # Class
class: Lcobucci\JWT\Validation\Constraint\ValidAt
Lcobucci\JWT\Validation\ValidAt: # Interface
class: Lcobucci\JWT\Validation\Constraint\StrictValidAt
arguments: [ '@Lcobucci\Clock\Clock' ]
# < --- lcobucci/jwt ---

# > --- atlance/jwt ---
# > --- atlance/jwt-core ---
Atlance\JwtCore\Token\Validation\Constraint\HasClaim: # Class
arguments: [ '%atlance_jwt_auth.client_claim_name%' ]

Atlance\JwtCore\Token\Validation\Contracts\ValidatorInterface: # Interface
class: Atlance\JwtCore\Token\Validation\Validator
arguments:
- '@Lcobucci\JWT\Validator'
- '@Lcobucci\JWT\Validation\SignedWith'
- '@Lcobucci\JWT\Validation\ValidAt'
- '@Atlance\JwtCore\Token\Validation\Constraint\HasClaim'

Atlance\JwtCore\Token\Contracts\Builder\Decorator\JWTBuilderInterface: # Interface
Atlance\JwtCore\Token\Contracts\Builder\Decorator\NullableArgsDecoratorInterface: # Interface
class: Atlance\JwtCore\Token\Builder
arguments:
$key: '@jwt_auth.private_key'
$ttl: '%atlance_jwt_auth.ttl%'
- '@Lcobucci\JWT\Builder'
- '@Lcobucci\JWT\Signer'
- '@jwt_auth.private_key'
- '@Lcobucci\Clock\Clock'
- '%atlance_jwt_auth.ttl%'

Atlance\JwtCore\Token\Contracts\Factory\TokenFactoryInterface: # Interface
class: Atlance\JwtCore\Token\Factory\TokenFactory
arguments: [ '@Atlance\JwtCore\Token\Contracts\Builder\Decorator\NullableArgsDecoratorInterface' ]

Atlance\JwtCore\Token\Contracts\DecodeInterface: # Interface
class: Atlance\JwtCore\Token\UseCase\Decode\Handler
Atlance\JwtCore\Token\Validation\Validator: # Class
arguments:
- '@Lcobucci\JWT\Validator'
- '@Lcobucci\JWT\Validation\SignedWith'
- '@Lcobucci\JWT\Validation\ValidAt'
- '@Atlance\JwtCore\Token\Validation\Constraint\HasClaim'

Atlance\JwtCore\Token\Contracts\EncodeInterface:
Atlance\JwtCore\Token\Contracts\EncodeInterface: # Interface
class: Atlance\JwtCore\Token\UseCase\Encode\Handler
# < --- atlance/jwt ---
arguments:
- '@Atlance\JwtCore\Token\Contracts\Factory\TokenFactoryInterface'
- '@Atlance\JwtCore\Token\Validation\Validator'

Atlance\JwtCore\Token\Contracts\DecodeInterface: # Interface
class: Atlance\JwtCore\Token\UseCase\Decode\Handler
arguments:
- '@Lcobucci\JWT\Parser'
- '@Atlance\JwtCore\Token\Validation\Validator'
# < --- atlance/jwt-core ---

# > --- atlance/jwt-auth ---
Atlance\JwtAuth\Security\Factory\Contracts\UserDataSetFactoryInterface: # Interface
class: Atlance\JwtAuth\Security\Factory\UserDataSetFactory
arguments: ['%atlance_jwt_auth.client_claim_name%']
arguments:
$identifierClaimName: '%atlance_jwt_auth.client_claim_name%'

Atlance\JwtAuth\Security\UseCase\Access\Token\HandlerInterface: # Interface
class: Atlance\JwtAuth\Security\UseCase\Access\Token\Handler
Expand All @@ -93,4 +104,3 @@ services:

Atlance\JwtAuth\Security\UseCase\Create\Token\HandlerInterface: # Interface
class: Atlance\JwtAuth\Security\UseCase\Create\Token\Handler
public: true
2 changes: 1 addition & 1 deletion src/Security/Factory/UserDataSetFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function __construct(string $identifierClaimName)
public function create(UserInterface $user): DataSetInterface
{
return DataSetFactory::fromHashTable([
$this->identifierClaimName => $user->getUsername(),
$this->identifierClaimName => $user->getUserIdentifier(),
'roles' => $user->getRoles(),
]);
}
Expand Down
13 changes: 7 additions & 6 deletions tests/Kernel/Infrastructure/Http/Controller/Login/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,33 @@
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;

/**
* @IsGranted("IS_AUTHENTICATED_ANONYMOUSLY")
*
* @Route("/login", name=Controller::class, methods={"POST"})
*/
final class Controller extends AbstractController
{
public function __invoke(
Request $request,
UserProviderInterface $provider,
UserPasswordEncoderInterface $encoder,
UserPasswordHasherInterface $hasher,
UseCase\Create\Token\HandlerInterface $handler
): JsonResponse {
/** @var array{username:string,password:string} $dataset */
$dataset = $this->jsonDecode($request);

try {
$user = $provider->loadUserByUsername($dataset['username']);
$encoder->isPasswordValid($user, $encoder->encodePassword($user, $dataset['password']));
$user = $provider->loadUserByIdentifier($dataset['username']);
$hasher->isPasswordValid($user, $hasher->hashPassword($user, $dataset['password']));

return new JsonResponse(['token' => $handler->handle($user)]);
} catch (UsernameNotFoundException $usernameNotFoundException) {
} catch (UserNotFoundException $userNotFoundException) {
return new JsonResponse(null, Response::HTTP_BAD_REQUEST);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@

/**
* @IsGranted("ROLE_USER")
* @Route(
* "/profile",
* name=Controller::class,
* methods={"GET"}
* )
*
* @Route("/profile", name=Controller::class, methods={"GET"})
*/
final class Controller extends AbstractController
class Controller extends AbstractController
{
public function __invoke(): JsonResponse
{
return new JsonResponse(['username' => $this->getUser()->getUsername()]);
return new JsonResponse(['username' => $this->getUser()->getUserIdentifier()]);
}
}
Loading

0 comments on commit de577da

Please sign in to comment.