Welcome to the first lesson in our course "Securing Your Symfony App". In this lesson, we'll focus on implementing secure user registration using Bcrypt for password hashing, a crucial part of any web application.
User registration with secure password storage helps in:
- Creating unique user accounts for personalized experiences.
- Enhancing security by controlling access to different parts of the application.
- Tracking user interactions and preferences.
- Protecting user credentials through secure password hashing.
By the end of this lesson, you will be able to set up a secure user registration feature in your Symfony application using Bcrypt for hashing passwords. This foundational knowledge will be valuable as we explore more advanced authentication strategies in future lessons.
Let's start by creating the User
entity, which will represent the users of our application.
php1<?php 2 3namespace App\Entity; 4 5use Doctrine\ORM\Mapping as ORM; 6 7#[ORM\Entity] 8#[ORM\Table(name: "users")] 9class User 10{ 11 #[ORM\Id] 12 #[ORM\GeneratedValue] 13 #[ORM\Column(type: "integer")] 14 private $id; 15 16 #[ORM\Column(type: "string", length: 50, unique: true)] 17 private $username; 18 19 #[ORM\Column(type: "string")] 20 private $password; 21 22 // Getters and setters... 23}
In this code, we define a User
entity class using Doctrine ORM annotations to specify how it maps to the underlying database. The #[ORM\Entity]
annotation indicates that this is a Doctrine entity, and the #[ORM\Table]
annotation specifies the table name and constraints.
The entity has three main properties: id
, username
, and password
. The id
is the primary key and is auto-generated. The username
is a unique string field, and the password
is also stored as a string. The class includes getter and setter methods to interact with these properties.
Next, we need a repository to interact with the user data in the database.
php1<?php 2 3namespace App\Repository; 4 5use App\Entity\User; 6use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; 7use Doctrine\Persistence\ManagerRegistry; 8 9class UserRepository extends ServiceEntityRepository 10{ 11 public function __construct(ManagerRegistry $registry) 12 { 13 parent::__construct($registry, User::class); 14 } 15 16 public function findByUsername(string $username): ?User 17 { 18 return $this->findOneBy(['username' => $username]); 19 } 20}
The UserRepository
extends Doctrine's ServiceEntityRepository
, which provides convenient methods for database operations. It includes a custom method findByUsername
to retrieve a user by their username, allowing us to encapsulate query logic within the repository.
The service layer provides a clean way to handle the application logic related to users. This allows for a clean separation between the controller logic and database operations.
php1<?php 2 3namespace App\Service; 4 5use App\Entity\User; 6use App\Repository\UserRepository; 7use Doctrine\ORM\EntityManagerInterface; 8 9class UserService 10{ 11 private $entityManager; 12 private $userRepository; 13 14 public function __construct(EntityManagerInterface $entityManager, UserRepository $userRepository) 15 { 16 $this->entityManager = $entityManager; 17 $this->userRepository = $userRepository; 18 } 19 20 public function create(array $data): User 21 { 22 $user = new User(); 23 $user->setUsername($data['username']); 24 // Hash the password using bcrypt 25 $hashedPassword = password_hash($data['password'], PASSWORD_BCRYPT); 26 $user->setPassword($hashedPassword); 27 $this->entityManager->persist($user); 28 $this->entityManager->flush(); 29 return $user; 30 } 31 32 public function findByUsername(string $username): ?User 33 { 34 return $this->userRepository->findByUsername($username); 35 } 36}
Here, the UserService
class uses dependency injection to receive the EntityManagerInterface
and UserRepository
instances. It has a main method create
, which creates a new user and saves it to the database after hashing the password using Bcrypt
. Using Bcrypt
for hashing ensures that passwords are securely stored, enhancing the security of our application. This service layer separates the business logic from the controller, making the code more modular and maintainable.
Bcrypt's adjustable cost factor
increases security by making brute-force attacks harder. The password_hash()
function uses Bcrypt with a default cost of 10, meaning it performs 2^10 (1024) hashing iterations. Although increasing the cost factor boosts security, it can slow down the login process, so it’s important to balance. You can customize the cost factor as shown:
php1$options = ['cost' => 12]; 2$hashedPassword = password_hash($data['password'], PASSWORD_BCRYPT, $options); 3$user->setPassword($hashedPassword);
This customization allows you to tailor the hashing process to meet specific security needs while considering performance.
The controller will handle HTTP requests and responses related to user registration.
php1<?php 2 3namespace App\Controller; 4 5use App\Service\UserService; 6use Symfony\Component\HttpFoundation\JsonResponse; 7use Symfony\Component\Routing\Annotation\Route; 8use Symfony\Component\HttpFoundation\Request; 9 10class UserController 11{ 12 private $userService; 13 14 public function __construct(UserService $userService) 15 { 16 $this->userService = $userService; 17 } 18 19 /** 20 * @Route("/users/register", name="register", methods={"POST"}) 21 */ 22 public function register(Request $request): JsonResponse 23 { 24 $data = json_decode($request->getContent(), true); 25 $user = $this->userService->create($data); 26 return new JsonResponse(['username' => $user->getUsername()]); 27 } 28}
In this code, we define a route for user registration that handles POST requests. The register
method creates a new user and returns the username in the response.
Finally, we need to configure the services in Symfony.
YAML1# app/config/services.yaml 2services: 3 # default configuration for services in *this* file 4 _defaults: 5 autowire: true # Automatically injects dependencies in your services. 6 autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 7 8 # makes classes in src/ available to be used as services 9 App\: 10 resource: '../src/' 11 exclude: 12 - '../src/DependencyInjection/' 13 - '../src/Entity/' 14 - '../src/Kernel.php' 15 16 App\Controller\UserController: 17 public: true 18 tags: ['controller.service_arguments']
In the above services.yaml
configuration, the line App\Controller\UserController: public: true tags: ['controller.service_arguments']
ensures that the UserController
is publicly accessible and that its constructor arguments are correctly injected. This is necessary for the controller to function properly within the Symfony framework.
In this lesson, we have covered:
- Entity Creation: We created the
User
entity with Doctrine ORM. - Repository Setup: We set up the
UserRepository
for database interactions. - Service Implementation: We built the
UserService
for handling business logic, including secure password hashing with Bcrypt. - Controller Development: We created the
UserController
to handle secure registration. - Configuration: We configured services.
Now that you have a foundational understanding of how to implement secure user registration in Symfony using Bcrypt, it's time to put this knowledge into practice. Use the provided examples and explanations to complete the exercises and reinforce your learning.