Lesson 1
Secure User Registration with Bcrypt
Secure User Registration with Bcrypt

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.

Setting Up the User Entity

Let's start by creating the User entity, which will represent the users of our application.

php
1<?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.

Creating the User Repository

Next, we need a repository to interact with the user data in the database.

php
1<?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.

Building the User Service

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.

php
1<?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.

Customizing Bcrypt’s Cost Factor

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:

php
1$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.

Setting Up the User Controller

The controller will handle HTTP requests and responses related to user registration.

php
1<?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.

Configuring Services

Finally, we need to configure the services in Symfony.

YAML
1# 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.

Summary

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.

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.