DEV Community

Cover image for Laravel101: A Step-by-Step Guide to Implementing a Simple Authentication System
Kazem
Kazem

Posted on

Laravel101: A Step-by-Step Guide to Implementing a Simple Authentication System

In this tutorial, we will walk you through the process of implementing a robust authentication system manually within your application.

In the Laravel ecosystem, there are numerous approaches to implementing authentication, ranging from session-based to API-based, and from lightweight packages like Sanctum to to more complex Passport. Additionally, there are handy starter kits such as Breeze and Jetstream, which provide a streamlined starting point. However, for the purpose of this beginner tutorial, we will focus on implementing a straightforward authentication system that is as simple as possible.


Let’s begin by registering a user.

To create our controller in a separate directory, use the following command:



php artisan make:controller Auth/RegisterController


Enter fullscreen mode Exit fullscreen mode

Inside of this controller we need two function. One function is responsible for displaying the view, while the other handles the controller’s logic.

Following REST architecture, we need a create method to display the registration view and a store method to handle the registration request:



class RegisterController extends Controller
{
    public function create()
    {
        return view('auth.register');
    }

    public function store()
    {
    }
}


Enter fullscreen mode Exit fullscreen mode

Then I have developed a simple user form that includes fields for name, email, and password input:

Image description

This form has just a simple layout layout.auth without our navbar. Additionally, I have put all the authentication forms into separate directory resources/views/auth

Now, let’s return to our routes and define the route for user registration.



Route::get("/register", [RegisterController::class, 'create'])->name('register');
Route::post("/register", [RegisterController::class, 'store'])->name('register.store');


Enter fullscreen mode Exit fullscreen mode

Next, we need to create a request which could look like bellow:



class RegisterRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => 'required|min:3',
            'email' => "required|email|unique:users,eamil",
            'password' => "required|min:4|max:20"
        ];
    }
}


Enter fullscreen mode Exit fullscreen mode

The email rule is in place to ensure that the email address provided follows the proper email format. The unique rule is used to make sure that the email entered has not been used before.

No we can simply use it in our store function:



public function store(RegisterRequest $request)
{
    $user = User::create($request->validated());

    Auth::login($user);

    return redirect("/tasks");
}


Enter fullscreen mode Exit fullscreen mode

Here, I make use of an awesome class called Auth to provide a simple login within created user. Additionally, there is another globally defined helper function for Auth like auth(), which you can use: auth()->login($user)

Now, let’s have some fun. Go back to the tasks store function and try to retrieve the currently logged-in user in the application. To do this, we can use the dd :



Auth::user()

// or 
auth()->user()

// or 

$request->user()


Enter fullscreen mode Exit fullscreen mode

Here’s how you can do it:

Image description

Amazing! This means that our user now logged in in thew application successfully!

The another thing you need to pay attention here is the mass assignment issue when using the create function for a model.

If you look at the User model class created with Laravel, it handles this issue by default:



protected $fillable = [
    'name',
    'email',
    'password',
];

protected $hidden = [
    'password',
    'remember_token',
];

protected $casts = [
    'email_verified_at' => 'datetime',
    'password' => 'hashed',
];


Enter fullscreen mode Exit fullscreen mode

As you can see in the code, the password is converted to a hash by casting. This is a secure and useful way to store passwords in the database.

Now, What if you want to use your own hash method in a different way? Well, you can do it inside our request class. Just before passing the data, remember that all the processes occur after validation, and it just happens by rewriting validated method:



namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Hash;

class RegisterRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'name' => "required|min:3",
            'email' => "required|email|unique:users,eamil",
            'password' => "required|min:4|max:20"
        ];
    }

    public function validated($key = null, $default = null)
    {
        return array_merge($this->validator->validated(), [
            'password' => Hash::make(request('password'))
        ]);
    }
}


Enter fullscreen mode Exit fullscreen mode

The Laravel Hash facade offers secure hashing using Bcrypt and Argon2 algorithms. For detailed instructions you can refer to the official Laravel documentation.

Now, let’s return to our registry controller and bring everything together:



namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Http\Requests\RegisterRequest;
use App\Models\User;

class RegisterController extends Controller
{
    public function create()
    {
        return view('auth.register');
    }

    public function store(RegisterRequest $request)
    {
        $user = User::create($request->validated());

        auth()->login($user);

        return redirect("/tasks");
    }
}


Enter fullscreen mode Exit fullscreen mode

It’s really clear, but in the real world, after a user is created, we typically send a verification email to the user. However, for now, let’s keep things simple and cover that in the next lessons. Okay, now let’s get started by moving on to the login controller:



php artisan make:controller Auth/LoginController


Enter fullscreen mode Exit fullscreen mode

Then let’s add routes :



Route::get('login', [LoginController::class, 'create'])->name('login');
Route::post('login', [LoginController::class, 'store'])->name('login.store');


Enter fullscreen mode Exit fullscreen mode

Just like in the register controller, we need to create a function in the login controller to display our login view, which I’ve kept simple from register form:

Image description

And here is our LoginRequest



class LoginRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'email' => 'required|email',
            'password' => 'required'
        ];
    }
}


Enter fullscreen mode Exit fullscreen mode

Finally let’s back to our login controller.

When it comes to creating a login system there are a few steps we need to follow. First, we check if the user exists in our database. Then, we verify if the entered password matches the hashed password stored in our records. If everything checks out and the user exists, we generate a session for them and grant access to the application.

You can handle all of the scenario by yourself, use another helper function called attempt in Auth facade that takes care of all these tasks. It’s quite simple to use. We just need to pass the user’s credentials and it handles the rest:



class LoginController extends Controller
{
    public function create()
    {
        return view('auth.login');
    }

    public function store(LoginRequest $request)
    {
        if (auth()->attempt($request->validated())) {
            return redirect()->intended('/tasks');
        }

        return back();
    }
}


Enter fullscreen mode Exit fullscreen mode

You can simply use the above code but if you want to handle exceptions and display error messages, you can make use of the helper function called withErrors. Just modify the code as follow:



public function store(LoginRequest $request)
{
    if (auth()->attempt($request->validated())) {
        return redirect()->intended('/tasks');
    }

    return back()
        ->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ]);
}


Enter fullscreen mode Exit fullscreen mode

Sure! If you’d like to go back to the view with the email value pre-filled, you can use the withInput function. Here's how you can do it:



return back()
    ->withInput($request->only('email'))
    ->withErrors([
        'email' => 'The provided credentials do not match our records.',
    ]);


Enter fullscreen mode Exit fullscreen mode

And this is the result:

Image description

Well, our next task is to finalize the authentication process and focus on implementing the logout feature. To begin, we’ll create a controller:



php artisan make:controller Auth/LogoutController


Enter fullscreen mode Exit fullscreen mode

And ensure its routes are properly defined:



Route::post('logout', LogoutController::class)->name('logout');


Enter fullscreen mode Exit fullscreen mode

Since we have only one function in controller, I suggest utilizing an invocable method:



namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;

class LogoutController extends Controller
{
    public function __invoke()
    {
        auth()->logout();

        return redirect('/login');
    }
}


Enter fullscreen mode Exit fullscreen mode

That’s really easy! Now, let’s create our logout view. It’s a simple form with a button that you can click to submit the logout request:



<form method="POST" action={{ route('logout') }}>
    @csrf
    <button type="submit">Logout</button>
</form>


Enter fullscreen mode Exit fullscreen mode

How can we use this view in our application? and when should we avoid using it?

Currently, the navbar just displays options for signing in and signing up. We need to display a logout view when the user is logged in and this these options when the user is unauthorized.

To achieve this, we have two functions in blade. The first one auth is used to display content exclusively for authenticated users, while the second one guest is specifically for guest users or users who haven’t been authorized yet:

Image description

That’s it!

One important point is that we don’t want guests to have access to the task resources. Additionally, we want the logout request to be accessible only to authorized users.

In the upcoming lessons, we will explore middleware in Laravel in detail. But for now, let’s briefly mention that it consists of rules that check the current request before it reaches the intended endpoint. In our case, we check if the current request is made by an authorized user, and if not, we want to return a 403 exception.

To implement this, we can use the auth middleware. Laravel provides a function to add middleware specifically to a route. Here’s an example:



Route::post('logout', LogoutController::class)->middleware('auth');


Enter fullscreen mode Exit fullscreen mode

Additionally, we can group multiple routes together and apply the same middleware to all of them. Here’s an example:



Route::group(['middleware' => 'auth'], function () {
    // Add your protected routes here
});


Enter fullscreen mode Exit fullscreen mode

Now, let’s move on to our web routes and add both the logout and tasks resources to a group:



Route::group(['middleware' => 'auth'], function () {
    Route::resource('tasks', TaskController::class);
    Route::post('logout', LogoutController::class)->name('logout');
});


Enter fullscreen mode Exit fullscreen mode

Great job! We have successfully implemented the authorization system in Laravel, and it was really straightforward and clear. While we can add many more features to it, let’s keep it simple for now.

You can find out the project code of this series tutorial in the repo.

Happy coding!

Top comments (0)