| @@ -16,12 +16,14 @@ security: | |||||
| auth: | auth: | ||||
| pattern: ^/api/auth | pattern: ^/api/auth | ||||
| stateless: true | stateless: true | ||||
| json_login: | |||||
| check_path: /api/auth | |||||
| username_path: email | |||||
| password_path: password | |||||
| success_handler: lexik_jwt_authentication.handler.authentication_success | |||||
| failure_handler: lexik_jwt_authentication.handler.authentication_failure | |||||
| custom_authenticators: | |||||
| - App\Security\JwtAuthenticator | |||||
| # json_login: | |||||
| # check_path: /api/auth | |||||
| # username_path: email | |||||
| # password_path: password | |||||
| # success_handler: lexik_jwt_authentication.handler.authentication_success | |||||
| # failure_handler: lexik_jwt_authentication.handler.authentication_failure | |||||
| api: | api: | ||||
| pattern: ^/api/ | pattern: ^/api/ | ||||
| stateless: true | stateless: true | ||||
| @@ -4,7 +4,7 @@ controllers: | |||||
| namespace: App\Controller | namespace: App\Controller | ||||
| type: attribute | type: attribute | ||||
| # Bestehende Auth-Route | |||||
| ## Bestehende Auth-Route | |||||
| auth: | auth: | ||||
| path: /api/auth | path: /api/auth | ||||
| methods: ['POST'] | methods: ['POST'] | ||||
| @@ -23,6 +23,14 @@ services: | |||||
| # add more service definitions when explicit configuration is needed | # add more service definitions when explicit configuration is needed | ||||
| # please note that last definitions always *replace* previous ones | # please note that last definitions always *replace* previous ones | ||||
| App\OpenApi\AuthDecorator: | |||||
| decorates: 'api_platform.openapi.factory' | |||||
| arguments: ['@.inner'] | |||||
| App\OpenApi\OpenApiFactory: | |||||
| decorates: 'api_platform.openapi.factory' | |||||
| arguments: [ '@.inner' ] | |||||
| acme_api.event.authentication_success_listener: | acme_api.event.authentication_success_listener: | ||||
| class: App\EventListener\AuthenticationSuccessListener | class: App\EventListener\AuthenticationSuccessListener | ||||
| tags: | tags: | ||||
| @@ -0,0 +1,16 @@ | |||||
| <?php | |||||
| // src/Dto/LoginRequest.php | |||||
| namespace App\Dto; | |||||
| use ApiPlatform\Metadata\ApiResource; | |||||
| use ApiPlatform\Metadata\ApiProperty; | |||||
| #[ApiResource] | |||||
| class LoginRequest | |||||
| { | |||||
| #[ApiProperty(example: "user@example.com")] | |||||
| public string $email = ''; | |||||
| #[ApiProperty(example: "password123")] | |||||
| public string $password = ''; | |||||
| } | |||||
| @@ -27,7 +27,20 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; | |||||
| shortName: 'MediaObject', | shortName: 'MediaObject', | ||||
| types: ['https://schema.org/MediaObject'], | types: ['https://schema.org/MediaObject'], | ||||
| operations: [ | operations: [ | ||||
| new Get(), | |||||
| new Get( | |||||
| openapi: new Model\Operation( | |||||
| responses: [ | |||||
| '200' => [ | |||||
| 'content' => [ | |||||
| 'application/ld+json' => [ | |||||
| 'schema' => ['$ref' => '#/components/schemas/MediaObject'] | |||||
| ] | |||||
| ] | |||||
| ] | |||||
| ] | |||||
| ), | |||||
| normalizationContext: ['groups' => ['media_object:read']] | |||||
| ), | |||||
| new GetCollection(), | new GetCollection(), | ||||
| new Post( | new Post( | ||||
| controller: CreateMediaObjectAction::class, | controller: CreateMediaObjectAction::class, | ||||
| @@ -0,0 +1,111 @@ | |||||
| <?php | |||||
| // src/OpenApi/AuthDecorator.php | |||||
| namespace App\OpenApi; | |||||
| use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; | |||||
| use ApiPlatform\OpenApi\Model\PathItem; | |||||
| use ApiPlatform\OpenApi\Model\Operation; | |||||
| use ApiPlatform\OpenApi\Model\RequestBody; | |||||
| use ApiPlatform\OpenApi\OpenApi; | |||||
| use ArrayObject; | |||||
| final class AuthDecorator implements OpenApiFactoryInterface | |||||
| { | |||||
| public function __construct( | |||||
| private OpenApiFactoryInterface $decorated | |||||
| ) {} | |||||
| public function __invoke(array $context = []): OpenApi | |||||
| { | |||||
| $openApi = ($this->decorated)($context); | |||||
| $schemas = $openApi->getComponents()->getSchemas(); | |||||
| $schemas['Credentials'] = new \ArrayObject([ | |||||
| 'type' => 'object', | |||||
| 'properties' => [ | |||||
| 'email' => [ | |||||
| 'type' => 'string', | |||||
| 'example' => 'user@example.com', | |||||
| ], | |||||
| 'password' => [ | |||||
| 'type' => 'string', | |||||
| 'example' => 'password123', | |||||
| ], | |||||
| ], | |||||
| ]); | |||||
| $schemas['AuthResponse'] = new \ArrayObject([ | |||||
| 'type' => 'object', | |||||
| 'properties' => [ | |||||
| 'token' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'id' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'email' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'firstName' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'lastName' => [ | |||||
| 'type' => 'string', | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'roles' => [ | |||||
| 'type' => 'array', | |||||
| 'items' => [ | |||||
| 'type' => 'string' | |||||
| ], | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| 'user' => [ | |||||
| '$ref' => '#/components/schemas/User', // API Platform's automatisch generiertes Schema | |||||
| 'readOnly' => true, | |||||
| ], | |||||
| ], | |||||
| ]); | |||||
| $pathItem = new PathItem( | |||||
| ref: 'JWT Token', | |||||
| post: new Operation( | |||||
| operationId: 'postCredentialsItem', | |||||
| tags: ['Auth'], | |||||
| responses: [ | |||||
| '200' => [ | |||||
| 'description' => 'Get JWT token', | |||||
| 'content' => [ | |||||
| 'application/json' => [ | |||||
| 'schema' => [ | |||||
| '$ref' => '#/components/schemas/AuthResponse', | |||||
| ], | |||||
| ], | |||||
| ], | |||||
| ], | |||||
| ], | |||||
| summary: 'Get JWT token to login.', | |||||
| requestBody: new RequestBody( | |||||
| description: 'Generate new JWT Token', | |||||
| content: new \ArrayObject([ | |||||
| 'application/json' => [ | |||||
| 'schema' => [ | |||||
| '$ref' => '#/components/schemas/Credentials', | |||||
| ], | |||||
| ], | |||||
| ]), | |||||
| ), | |||||
| security: [], | |||||
| ), | |||||
| ); | |||||
| $openApi->getPaths()->addPath('/api/auth', $pathItem); | |||||
| return $openApi; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,34 @@ | |||||
| <?php | |||||
| namespace App\OpenApi; | |||||
| use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; | |||||
| use ApiPlatform\OpenApi\OpenApi; | |||||
| use ApiPlatform\OpenApi\Model; | |||||
| class OpenApiFactory implements OpenApiFactoryInterface | |||||
| { | |||||
| public function __construct( | |||||
| private OpenApiFactoryInterface $decorated | |||||
| ) {} | |||||
| public function __invoke(array $context = []): OpenApi | |||||
| { | |||||
| $openApi = ($this->decorated)($context); | |||||
| // Cleanup schema names | |||||
| $schemas = $openApi->getComponents()->getSchemas(); | |||||
| $cleanedSchemas = new \ArrayObject(); | |||||
| foreach ($schemas as $key => $schema) { | |||||
| // Remove the .jsonld-media_object.read suffix | |||||
| $newKey = preg_replace('/\.jsonld-.*/', '', $key); | |||||
| $cleanedSchemas[$newKey] = $schema; | |||||
| } | |||||
| $openApi = $openApi->withComponents( | |||||
| $openApi->getComponents()->withSchemas($cleanedSchemas) | |||||
| ); | |||||
| return $openApi; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,75 @@ | |||||
| <?php | |||||
| // src/Security/JwtAuthenticator.php | |||||
| namespace App\Security; | |||||
| use App\ApiResource\UserApi; | |||||
| use App\Repository\UserRepository; | |||||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | |||||
| use Symfony\Component\HttpFoundation\JsonResponse; | |||||
| use Symfony\Component\HttpFoundation\Request; | |||||
| use Symfony\Component\HttpFoundation\Response; | |||||
| use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |||||
| use Symfony\Component\Security\Core\Exception\AuthenticationException; | |||||
| use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | |||||
| use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | |||||
| use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |||||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||||
| class JwtAuthenticator extends AbstractAuthenticator | |||||
| { | |||||
| public function __construct( | |||||
| private JWTTokenManagerInterface $jwtManager, | |||||
| private UserRepository $userRepository, | |||||
| private MicroMapperInterface $microMapper | |||||
| ) {} | |||||
| public function supports(Request $request): ?bool | |||||
| { | |||||
| return $request->getPathInfo() === '/api/auth' && $request->isMethod('POST'); | |||||
| } | |||||
| public function authenticate(Request $request): Passport | |||||
| { | |||||
| $data = json_decode($request->getContent(), true); | |||||
| if (!$data) { | |||||
| throw new CustomUserMessageAuthenticationException('Invalid JSON.'); | |||||
| } | |||||
| $email = $data['email'] ?? ''; | |||||
| $password = $data['password'] ?? ''; | |||||
| return new Passport( | |||||
| new UserBadge($email), | |||||
| new PasswordCredentials($password) | |||||
| ); | |||||
| } | |||||
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |||||
| { | |||||
| $user = $token->getUser(); | |||||
| $jwtToken = $this->jwtManager->create($user); | |||||
| return new JsonResponse([ | |||||
| 'token' => $jwtToken, | |||||
| 'id' => $user->getId(), | |||||
| 'email' => $user->getEmail(), | |||||
| 'firstName' => $user->getFirstName(), | |||||
| 'lastName' => $user->getLastName(), | |||||
| 'roles' => $user->getRoles(), | |||||
| 'user' => $this->microMapper->map( | |||||
| $user, UserApi::class | |||||
| ) | |||||
| ]); | |||||
| } | |||||
| public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |||||
| { | |||||
| return new JsonResponse([ | |||||
| 'message' => $exception->getMessage() | |||||
| ], Response::HTTP_UNAUTHORIZED); | |||||
| } | |||||
| } | |||||