# Symfony Security Advanced Configuration ## Authentication Systems ### JWT Authentication ```php // Install required packages // composer require lexik/jwt-authentication-bundle // config/packages/lexik_jwt_authentication.yaml 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 // config/packages/security.yaml security: firewalls: login: pattern: ^/api/login stateless: true json_login: check_path: /api/login_check success_handler: lexik_jwt_authentication.handler.authentication_success failure_handler: lexik_jwt_authentication.handler.authentication_failure api: pattern: ^/api stateless: true jwt: ~ ``` ### OAuth2 Implementation ```php // Using KnpU OAuth2 Client Bundle // composer require knpuniversity/oauth2-client-bundle // config/packages/knpu_oauth2_client.yaml knpu_oauth2_client: clients: google: type: google client_id: '%env(GOOGLE_CLIENT_ID)%' client_secret: '%env(GOOGLE_CLIENT_SECRET)%' redirect_route: connect_google_check redirect_params: {} // Controller for OAuth #[Route('/connect/google', name: 'connect_google')] public function connectGoogle(ClientRegistry $clientRegistry): Response { return $clientRegistry ->getClient('google') ->redirect(['email', 'profile']); } #[Route('/connect/google/check', name: 'connect_google_check')] public function connectGoogleCheck(Request $request, ClientRegistry $clientRegistry): Response { $client = $clientRegistry->getClient('google'); $user = $client->fetchUser(); // Handle user creation/authentication // ... } ``` ### Two-Factor Authentication ```php // composer require scheb/2fa-bundle scheb/2fa-totp // Entity with 2FA #[ORM\Entity] class User implements UserInterface, TwoFactorInterface { #[ORM\Column(nullable: true)] private ?string $totpSecret = null; public function isTotpAuthenticationEnabled(): bool { return $this->totpSecret !== null; } public function getTotpAuthenticationUsername(): string { return $this->email; } public function getTotpAuthenticationConfiguration(): ?TotpConfigurationInterface { return new TotpConfiguration($this->totpSecret, TotpConfiguration::ALGORITHM_SHA1, 30, 6); } } // config/packages/security.yaml security: firewalls: main: two_factor: auth_form_path: 2fa_login check_path: 2fa_login_check ``` ## Custom Authenticators ### API Key Authenticator ```php namespace App\Security; 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\Http\Authenticator\AbstractAuthenticator; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; use Symfony\Component\Security\Http\Authenticator\Passport\Passport; use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport; class ApiKeyAuthenticator extends AbstractAuthenticator { public function __construct( private UserRepository $userRepository ) {} public function supports(Request $request): ?bool { return $request->headers->has('X-API-KEY'); } public function authenticate(Request $request): Passport { $apiKey = $request->headers->get('X-API-KEY'); if (null === $apiKey) { throw new CustomUserMessageAuthenticationException('No API key provided'); } return new SelfValidatingPassport( new UserBadge($apiKey, function($apiKey) { $user = $this->userRepository->findOneBy(['apiKey' => $apiKey]); if (!$user) { throw new CustomUserMessageAuthenticationException('Invalid API Key'); } return $user; }) ); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { return null; } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { return new JsonResponse([ 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) ], Response::HTTP_UNAUTHORIZED); } } ``` ## Advanced Voters ### Hierarchical Voters ```php namespace App\Security\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class DocumentVoter extends Voter { public const VIEW = 'DOCUMENT_VIEW'; public const EDIT = 'DOCUMENT_EDIT'; public const DELETE = 'DOCUMENT_DELETE'; public const SHARE = 'DOCUMENT_SHARE'; protected function supports(string $attribute, mixed $subject): bool { return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE, self::SHARE]) && $subject instanceof Document; } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { $user = $token->getUser(); if (!$user instanceof User) { return false; } /** @var Document $document */ $document = $subject; // Check hierarchical permissions if ($this->hasHierarchicalAccess($user, $document, $attribute)) { return true; } return match($attribute) { self::VIEW => $this->canView($document, $user), self::EDIT => $this->canEdit($document, $user), self::DELETE => $this->canDelete($document, $user), self::SHARE => $this->canShare($document, $user), default => false, }; } private function hasHierarchicalAccess(User $user, Document $document, string $attribute): bool { // Check department hierarchy if ($user->isDepartmentHead() && $document->getDepartment() === $user->getDepartment()) { return true; } // Check organization hierarchy if ($user->isOrganizationAdmin()) { return true; } return false; } private function canView(Document $document, User $user): bool { // Public documents if ($document->isPublic()) { return true; } // Owner can view if ($document->getOwner() === $user) { return true; } // Shared with user if ($document->getSharedUsers()->contains($user)) { return true; } // Team member can view team documents if ($document->getTeam() && $document->getTeam()->hasMember($user)) { return true; } return false; } private function canEdit(Document $document, User $user): bool { // Owner can edit if ($document->getOwner() === $user) { return true; } // Check edit permissions return $document->hasEditPermission($user); } private function canDelete(Document $document, User $user): bool { // Only owner and admins can delete return $document->getOwner() === $user || $user->hasRole('ROLE_ADMIN'); } private function canShare(Document $document, User $user): bool { // Owner and users with share permission return $document->getOwner() === $user || $document->hasSharePermission($user); } } ``` ## Role Hierarchy & Dynamic Roles ### Dynamic Role Provider ```php namespace App\Security; use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; class DynamicRoleHierarchy implements RoleHierarchyInterface { public function __construct( private RoleRepository $roleRepository ) {} public function getReachableRoleNames(array $roles): array { $reachableRoles = $roles; foreach ($roles as $role) { $roleEntity = $this->roleRepository->findOneBy(['name' => $role]); if ($roleEntity) { // Add inherited roles foreach ($roleEntity->getInheritedRoles() as $inheritedRole) { $reachableRoles[] = $inheritedRole->getName(); } // Add permission-based roles foreach ($roleEntity->getPermissions() as $permission) { $reachableRoles[] = 'ROLE_' . strtoupper($permission->getName()); } } } return array_unique($reachableRoles); } } ``` ## Security Event Listeners ### Login Success Handler ```php namespace App\Security; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface { public function __construct( private EntityManagerInterface $em, private LoggerInterface $logger, private IpGeolocationService $geolocation ) {} public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response { $user = $token->getUser(); // Log successful login $loginLog = new LoginLog(); $loginLog->setUser($user); $loginLog->setIpAddress($request->getClientIp()); $loginLog->setUserAgent($request->headers->get('User-Agent')); $loginLog->setTimestamp(new \DateTimeImmutable()); // Get geolocation $location = $this->geolocation->locate($request->getClientIp()); $loginLog->setLocation($location); // Check for suspicious activity if ($this->isSuspiciousLogin($user, $location)) { $this->notifyUserOfSuspiciousActivity($user, $loginLog); } // Update last login $user->setLastLoginAt(new \DateTimeImmutable()); $user->setLastLoginIp($request->getClientIp()); $this->em->persist($loginLog); $this->em->flush(); // Log event $this->logger->info('User logged in', [ 'user' => $user->getUserIdentifier(), 'ip' => $request->getClientIp() ]); return new RedirectResponse('/dashboard'); } private function isSuspiciousLogin(User $user, ?Location $location): bool { // Check if login from new country $lastLogins = $this->em->getRepository(LoginLog::class) ->findLastLogins($user, 10); foreach ($lastLogins as $login) { if ($login->getLocation() && $login->getLocation()->getCountry() === $location->getCountry()) { return false; } } return true; } } ``` ## Access Control Lists (ACL) ### Custom ACL Implementation ```php namespace App\Security\Acl; class AclManager { public function __construct( private EntityManagerInterface $em ) {} public function grantAccess( object $domainObject, UserInterface $user, array $permissions ): void { $acl = new Acl(); $acl->setObjectClass(get_class($domainObject)); $acl->setObjectId($domainObject->getId()); $acl->setUser($user); $acl->setPermissions($permissions); $this->em->persist($acl); $this->em->flush(); } public function revokeAccess( object $domainObject, UserInterface $user ): void { $acl = $this->em->getRepository(Acl::class)->findOneBy([ 'objectClass' => get_class($domainObject), 'objectId' => $domainObject->getId(), 'user' => $user ]); if ($acl) { $this->em->remove($acl); $this->em->flush(); } } public function isGranted( string $permission, object $domainObject, UserInterface $user ): bool { $acl = $this->em->getRepository(Acl::class)->findOneBy([ 'objectClass' => get_class($domainObject), 'objectId' => $domainObject->getId(), 'user' => $user ]); return $acl && in_array($permission, $acl->getPermissions()); } } ``` ## Security Headers & CORS ### Security Headers Subscriber ```php namespace App\EventSubscriber; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\KernelEvents; class SecurityHeadersSubscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ KernelEvents::RESPONSE => 'onKernelResponse', ]; } public function onKernelResponse(ResponseEvent $event): void { $response = $event->getResponse(); // Content Security Policy $response->headers->set( 'Content-Security-Policy', "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" ); // XSS Protection $response->headers->set('X-XSS-Protection', '1; mode=block'); // Prevent MIME sniffing $response->headers->set('X-Content-Type-Options', 'nosniff'); // Clickjacking protection $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); // HTTPS enforcement $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); // Referrer Policy $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); } } ``` ### CORS Configuration ```yaml # config/packages/nelmio_cors.yaml nelmio_cors: defaults: origin_regex: true allow_origin: ['%env(CORS_ALLOW_ORIGIN)%'] allow_methods: ['GET', 'OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'] allow_headers: ['Content-Type', 'Authorization', 'X-API-KEY'] expose_headers: ['Link', 'X-Total-Count'] max_age: 3600 paths: '^/api/': allow_origin: ['*'] allow_headers: ['*'] allow_methods: ['POST', 'PUT', 'GET', 'DELETE', 'OPTIONS'] max_age: 3600 ``` ## Rate Limiting ```php namespace App\Security; use Symfony\Component\RateLimiter\RateLimiterFactory; use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException; class RateLimitingService { public function __construct( private RateLimiterFactory $apiLimiter, private RateLimiterFactory $loginLimiter ) {} public function checkApiLimit(string $apiKey): void { $limiter = $this->apiLimiter->create($apiKey); if (!$limiter->consume(1)->isAccepted()) { throw new TooManyRequestsHttpException( $limiter->getRetryAfter()->getTimestamp() - time(), 'API rate limit exceeded' ); } } public function checkLoginLimit(string $username, string $ip): void { $limiter = $this->loginLimiter->create($username . '_' . $ip); if (!$limiter->consume(1)->isAccepted()) { throw new TooManyRequestsHttpException( $limiter->getRetryAfter()->getTimestamp() - time(), 'Too many login attempts' ); } } } // config/packages/rate_limiter.yaml framework: rate_limiter: api: policy: 'sliding_window' limit: 100 interval: '60 minutes' login: policy: 'fixed_window' limit: 5 interval: '15 minutes' ``` ## Encryption & Hashing ### Field-level Encryption ```php namespace App\Security; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; class EncryptionService { private string $key; public function __construct(string $encryptionKey) { $this->key = $encryptionKey; } public function encrypt(string $data): string { $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc')); $encrypted = openssl_encrypt($data, 'aes-256-cbc', $this->key, 0, $iv); return base64_encode($encrypted . '::' . $iv); } public function decrypt(string $data): string { list($encrypted_data, $iv) = explode('::', base64_decode($data), 2); return openssl_decrypt($encrypted_data, 'aes-256-cbc', $this->key, 0, $iv); } } // Doctrine Type for encrypted fields class EncryptedStringType extends Type { public function convertToDatabaseValue($value, AbstractPlatform $platform) { return $this->encryptionService->encrypt($value); } public function convertToPHPValue($value, AbstractPlatform $platform) { return $this->encryptionService->decrypt($value); } } ``` ## Security Best Practices 1. **Always use HTTPS in production** 2. **Implement CSRF protection for forms** 3. **Use parameterized queries to prevent SQL injection** 4. **Validate and sanitize all user input** 5. **Implement proper session management** 6. **Use strong password policies** 7. **Implement account lockout mechanisms** 8. **Log security events** 9. **Regular security audits** 10. **Keep dependencies updated**