You can create your own Authentication System in Laravel without using any Laravel package. In this topic, we will develop a Laravel custom authentication system using MySQL database. We will have login and registration, email verification, change password, forgot password, and reset password using custom code only without using any Laravel package. As we will not use any Laravel authentications like Breeze or Jetstream, we call this a Laravel custom authentication system.
Watch YouTube Video
Create a Laravel project
Let us create a project using the Laravel create project command. You need to have Composer and PHP installed in your system. We name the project as lara_custom_login.
composer create-project --prefer-dist laravel/laravel lara_custom_login
This will create the folder "lara_custom_login" under the folder where you run the above command.
MySQL Database and Migration
Update .env
file for database details. We will use a MySQL database named 'lara_user' for this example, so we updated the database details as below:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=lara_user
DB_USERNAME=root
DB_PASSWORD=
We will use the Laravel delivered "users" table for login and registration. Also, we will create a custom table called 'user_tokens' for email verification. Let us create a Laravel migration for this table. Run the below command for migration from the VS code terminal.
php artisan make:model UserToken -m
The above command will create the migration file <yyyy_mm_dd_xxxxxx>_create_user_tokens_table.php
under the database/migrations folder and a Model named "UserToken" under the app/Models folder.
Migration scripts for user_tokens
<?php
public function up()
{
Schema::create('user_tokens', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->text('token');
$table->timestamps();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('user_tokens');
}
Let us now run the Laravel migration to create our custom and the default Laravel tables. Run the below command to create the tables using php artisam migrate
.
php artisan migrate
After running the migration, below tables are created.
Create Laravel Models
We are not going to change the Laravel User model. We already have created the UserToken model during migration. We will now create a PasswordReset model.
php artisan make:model PasswordReset
Below are the models:
class PasswordReset extends Model
{
use HasFactory;
protected $fillable = ['email', 'token'];
protected $hidden = [
'token',
];
}
class UserToken extends Model
{
use HasFactory;
protected $fillable = ['user_id', 'token'];
protected $hidden = [
'token',
];
public function user()
{
return $this->belongsTo(User::class);
}
}
Write Controller methods
We will create a controller named "AuthController" for login, registration, forget password, etc. and another controller for the home page and dashboard, we will name it as "HomeController".
Let us run the Laravel make controller command from the terminal:
php artisan make:controller AuthController
php artisan make:controller HomeController
HomeController has two methods for the home page and the dashboard. Note that for the dashboard, the user must login to access it. Accordingly, in our route, we will place it under auth middleware. Below is our HomeControlller:
class HomeController extends Controller
{
public function index(){
$title = "Home";
return view('index', compact('title'));
}
public function dashboard(){
$title = "Dashboard";
return view ('dashboard', compact('title'));
}
}
AuthController
Let us take a look at AuthController. In this controller, we will have methods for the below functionalities:
- Register
- Login
- Verify Email
- Change Password
- Forget Password
- Logout
Register
We will take the name, email and password for registration, validate them and create a row in the "users" table. Also, we will create a token and send an email to the user's mailbox with a verification email link. When the user clicks the link, the email will be verified and user registration will be completed.
Below is the controller method for registering the user:
public function postRegister(Request $request)
{
$request->validate(
[
'name' => 'required|max:255',
'email' => 'required|email|unique:users|max:255',
'password' => 'required|min:6',
'confirm_password' => 'required|same:password',
]
);
try {
$user = new User;
$user->name = $request->name;
$user->email = $request->email;
$user->password = Hash::make($request->password);
$user->save();
// generate a token
$token = Str::random(64);
UserToken::create([
'user_id' => $user->id,
'token' => $token,
]);
Mail::send('emails.verify_email', ['token' => $token], function ($message) use ($request) {
$message->to($request->email);
$message->subject('Email Verification Mail');
});
return redirect("register")->withSuccess('A verification email is sent to ' . $request->email . ', please click the link in the email to verify your email.');
} catch (Exception $e) {
return redirect("register")->withErrors('Some Error occurred, please try later');
}
}
You can see that after creating a row in the "users" table, a random token is generated and sent to the user's email'. Email template "verify_email.blade.php" is created as below:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
h1 {
color: rgb(43, 21, 235);
}
</style>
</head>
<body>
<h1>Email Verification Mail</h1>
<h4>Thank you for Registering.</h4>
Please click on below link to verify your email:<br>
<a href="{{ route('user.verify', $token) }}">Verify Email</a>
</body>
</html>
Below is a sample email I received in my mailtrap account:
When the user clicks on the Verify Email link, we have another method verifyEmail()
to update the user as verified in the "users" table.
public function verifyEmail($token)
{
$verifyUser = UserToken::where('token', $token)->first();
if (!is_null($verifyUser)) {
$verifyUser->user->email_verified_at = Carbon::now();
$verifyUser->user->save();
// delete token
$verifyUser->delete();
$user = User::find($verifyUser->user_id);
Auth::login($user);
$message = "Email verified Successfully";
session(['user_name' => $user->name, 'user_id' => $user->id, 'user_email' => $user->email]);
return redirect('dashboard')->withSuccess($message);
} else {
$message = 'Token Error: Email can not be verified.';
return redirect()->route('page.error')->withError($message);
}
}
It takes the token as an input parameter and checks if it exists in the user_tokens table. If it finds the token, it updates the "email_verified_at" column in the "users" table and deletes the token. Then, automatic login happens and the user is redirected to the dashboard.
Login
In the login method, we must check if the email is verified. If the email is verified, we will check if the email/password is correct and allow login accordingly. But if the email is not verified, we create a token again and send an email with the link asking the user to click on the link from his/her mailbox. This check is required as the user may register but did not get the email verified and try to login without email verification.
Controller code for Login:
public function postLogin(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
if (DB::table('users')->where('email', $request->email)->doesntExist()) {
return back()->withInput()->withErrors([
'message' => 'Email id is not registered.',
]);
} else {
$user = User::where('email', $request->email)->first();
if ($user->email_verified_at) { // verified user
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
$user = Auth::user();
return redirect()->intended('dashboard');
} else {
return back()->withInput()->withErrors([
'message' => 'The provided credentials do not match with our records.',
]);
}
} else { // email is not verified
// generate a new token and send email for verification
$token = Str::random(64);
UserToken::UpdateOrCreate([
'user_id' => $user->id,
],
[
'token' => $token,
]);
Mail::send('emails.verify_email', ['token' => $token], function ($message) use ($request) {
$message->to($request->email);
$message->subject('Email Verification Mail');
});
return back()->withInput()->withErrors([
'message' => 'Your email is not verified yet. Please check email sent to ' . $request->email . ' and click on verify email link.',
]);
}
}
}
Change Password
For "Change Password", we will validate the current password and make sure that the new password is different from the current password.
public function changePasswordPost(Request $request)
{
$request->validate([
'current_password' => 'required',
'new_password' => 'required|min:6',
'confirm_password' => 'required|same:new_password|min:6',
]);
if (!(Hash::check($request->current_password, Auth::user()->password))) {
return redirect()->back()->with("error", "Your current password does not match with the password you entered.");
}
if (strcmp($request->current_password, $request->new_password) == 0) {
// Current password and new password same
return redirect()->back()->with("error", "New Password cannot be same as your current password.");
}
$user = User::find(Auth::user()->id);
$user->password = Hash::make($request->new_password);
$user->save();
return redirect()->back()->with("success", "Password successfully changed!");
}
Forgot Password
The user enters the registered email id in the forgot password form. After submitting, an email is sent to the user's mailbox to reset the password. The user gives a new password in the reset password form to reset his/her password. Below are the controller methods for Forgot and Reset Password.
public function forgetPasswordPost(Request $request)
{
$request->validate([
'email' => 'required|email|exists:users',
]);
$token = Str::random(64);
PasswordReset::updateOrCreate([
'email' => $request->email,
],
[
'token' => $token,
]);
Mail::send('emails.forget_password', ['token' => $token], function ($message) use ($request) {
$message->to($request->email);
$message->subject('Reset Password');
});
return back()->with('message', 'We have e-mailed your password reset link!');
}
public function resetPasswordForm($token)
{
$title = "Reset Password";
return view('auth.reset_password_form', ['token' => $token], compact('title'));
}
public function resetPasswordPost(Request $request)
{
$request->validate([
'email' => 'required|email|exists:users',
'new_password' => 'required|min:6',
'conf_new_password' => 'required|same:new_password|min:6',
]);
$verifyToken = DB::table('password_resets')
->where([
'email' => $request->email,
'token' => $request->token
])
->first();
if (!$verifyToken) {
return back()->withInput()->with('error', 'Invalid Token!');
}
User::where('email', $request->email)
->update(['password' => Hash::make($request->new_password)]);
DB::table('password_resets')->where(['email' => $request->email])->delete();
return redirect('login')->with('message', 'Your password is Reset. Please login with new password!');
}
In the forgetPasswordPost()
method, we create a token for the email id and send an email to the user with a link to reset the password. When the user clicks on the link, resetPasswordForm()
method is called. We have given the routes in the next section. When the user submits with the new password, we verify the token and update the password accordingly. Finally, we delete the token for the email.
Routes
Route::get('/',[HomeController::class,'index'])->name('home');
Route::get('login', [AuthController::class, 'index'])->name('login');
Route::post('login', [AuthController::class, 'postLogin'])->name('login.post');
Route::get('register', [AuthController::class, 'register'])->name('register');
Route::post('register', [AuthController::class, 'postRegister'])->name('register.post');
Route::get('user/verify/{token}', [AuthController::class, 'verifyEmail'])->name('user.verify');
Route::get('page/error', [AuthController::class, 'showErrorPage'])->name('page.error');
Route::get('forget-password', [AuthController::class, 'forgetPasswordForm'])->name('forget.password');
Route::post('forget-password', [AuthController::class, 'forgetPasswordPost'])->name('forget.password.post');
Route::get('reset-password/{token}', [AuthController::class, 'resetPasswordForm'])->name('reset.password.get');
Route::post('reset-password', [AuthController::class, 'resetPasswordPost'])->name('reset.password.post');
Route::middleware('auth:web')->group(function(){
Route::get('logout', [AuthController::class, 'logout'])->name('logout');
Route::get('dashboard', [HomeController::class, 'dashboard'])->name('dashboard');
Route::get('change-password', [AuthController::class, 'changePasswordForm'])->name('password.change.form');
Route::post('change-password', [AuthController::class, 'changePasswordPost'])->name('password.change.post');
});
Laravel blade Views
We have the below views in the resources/views folder
resources/views/layouts/header.blade.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>{{$title}}</title>
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
<meta name="_token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" />
<link rel="stylesheet" type="text/css" media="screen" href="{{asset('css/style.css')}}" />
</head>
resources/views/layouts/footer.blade.php
<div class="copyright">
<p>
© Copyright 2023 by xyz.com. All Rights Reserved.
</p>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
resources/views/layouts/master.blade.php
@include('layouts.header')
<body>
<div class="container">
<div class="row">
<div class="col-md-12">
<div class="navbar_inner">
<nav class="navbar navbar-expand-lg">
<a class="navbar-brand" href="{{ route('home') }}
">
Website Logo
</a>
<ul class="d-flex justify-content-end w-100">
@auth
<div>Welcome {{Auth::user()->name}}
</div>
<li>
<a class="custom-btn" href="{{ route('logout') }}"> Logout</a>
</li>
<li >
<a class="custom-btn" href="{{ route('password.change.form') }}"> Change Password</a>
</li>
@endauth
@guest
<li>
<a class="custom-btn" href="{{ route('login') }}"> Login</a>
</li>
<li>
<a class="custom-btn" href="{{ route('register') }}">Registration</a>
</li>
@endguest
</ul>
</nav>
</div>
</div>
</div>
@yield('main-content')
</div>
@include('layouts.footer')
@stack('js')
</body>
</html>
resources/views/index.blade.php
@extends('layouts.master')
@section('main-content')
<div class="container home">
<h1>Home</h1>
<p> @if(Session::has('message'))
<div class="alert alert-success">
{{ Session::get('message') }}
@php
Session::forget('message');
@endphp
</div>
@endif</p>
</div>
@endsection()
resources/views/dashboard.blade.php
@extends("layouts.master")
@section("main-content")
<div class="container home">
<h1>Dashboard</h1>
@if (session("success"))
<div class="alert alert-success">
{{ session("success") }}
</div>
@endif
</div>
@endsection()
resources/views/error_page.blade.php
@extends("layouts.master")
@section("main-content")
<div class="container home">
<h1>Error</h1>
<h3> @if (session("error"))
<div class="alert alert-danger">
{{ session("error") }}
</div>
@endif</h3>
<a href="{{route("home")}}">Go to Home</a>
</div>
@endsection()
resources/views/auth/register.blade.php
@extends('layouts.master')
@section('main-content')
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card">
<div class="card-header">
<h4>Register</h4>
</div>
<div class="card-body">
@if (session('success'))
<div class="alert alert-success">
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<form id = "thisForm" class="formRegister" action="{{ route('register.post') }}" method="post">
@csrf
<div class="form-group mb-3">
<label for="name">Name</label>
<input type="text" placeholder="Enter Name" name="name" class="form-control"
value="{{ old('name') }}" />
<div class="error">
@error('name')
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mb-3">
<label for="email">Email</label>
<input type="text" name="email" placeholder="Email Address" class="form-control"
value="{{ old('email') #125;}" />
<div class="error">
@error('email')
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mb-3">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Password" />
<div class="error">
@error('password')
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mb-3">
<label for="confirm_password">Confirm Password</label>
<input type="password" class="form-control" name="confirm_password"
placeholder="Confirm Password" />
<div class="error">
@error('confirm_password')
{{ $message }}
@enderror
</div>
</div>
<div class="form-group d-flex justify-content-end">
<button type="submit" class="submit-btn">Create My Account</button>
</div>
<div id ="loader"></div>
<div class="mt-5">
<p>Already have an account? <a href="{{ route('login') }}" class="create_now">Login</a></p>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('js')
<script>
$("#thisForm").submit(function() {
$(".submit-btn").attr("disabled", true);
$("#loader").show();
});
</script>
@endpush
Note that there is a loader added after form submit.
resources/views/auth/login.blade.php
@extends("layouts.master")
@section("main-content")
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card">
<div class="card-header"><h4>Login</h4></div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if (session()->has("message"))
<div class="alert alert-success">
{{ session()->get("message") }}
</div>
@endif
<form class="formLogin" action="{{ route('login.post') }}" method="post">
@csrf
<div class="form-group mb-4">
<label for="email">Email</label>
<input type="text" name="email" class="form-control" placeholder="Enter your Email"
value="{{ old('email') }}" />
</div>
<div class="form-group mb-3">
<label for="password">Password</label>
<input type="password" class="form-control" name="password" placeholder="Enter Password" />
</div>
<div class="form-group mt-5">
<button type="submit" class="submit-btn">Log in</button>
</div>
<div class="mt-4">
<a href="{{ route('forget.password') }}">Forgot password?</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
resources/views/auth/change_password.blade.php
@extends("layouts.master")
@section("main-content")
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card">
<div class="card-header">
<h4>Change Password</h4>
</div>
<div class="card-body">
@if (session("error"))
<div class="alert alert-danger">
{{ session("error") }}
</div>
@endif
@if (session("success"))
<div class="alert alert-success">
{{ session("success") }}
</div>
@endif
<form class="form-horizontal" method="POST" action="{{ route('password.change.post') }}">
{{ csrf_field() }}
<div class="form-group mt-3">
<label for="new-password">Current Password</label>
<input id="current_password" type="password" class="form-control" name="current_password">
<div class="error">
@error("current_password")
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mt-3">
<label for="new-password">New Password</label>
<input id="new_password" type="password" class="form-control" name="new_password">
<div class="error">
@error("new_password")
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mt-3">
<label for="new-password-confirm">Confirm New
Password</label>
<input id="new-password-confirm" type="password" class="form-control" name="confirm_password">
<div class="error">
@error("confirm_password")
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mt-5">
<button type="submit" class="btn btn-primary">
Change Password
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
resources/views/auth/forget_password.blade.php
@extends("layouts.master")
@section("main-content")
<div class="row justify-content-center">
<div class="col-md-7">
<div class="card">
<div class="card-header"><h4>Forgot Password?</h4></div>
<div class="card-body">
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if (session()->has("message"))
<div class="alert alert-success">
{{ session()->get("message") }}
</div>
@endif
<form id ="thisForm" action="{{ route('forget.password.post') }}" method="post">
@csrf
<div class="form-group">
<label>
Enter your email below to recieve a password Reset Link in your email</label>
<input type="text" name="email" placeholder="Enter your registered email"
class="form-control" />
</div>
<div class="form-group d-flex justify-content-end mt-5">
<button type="submit" class="submit-btn">Submit</button>
</div>
<div id ="loader"></div>
<div class="">
<p>Don't have an account yet? <a href="{{ route('register') }}" class="create_now">Create
Now</a></p>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
@push("js")
<script>
$("#thisForm").submit(function() {
$(".submit-btn").attr("disabled", true);
$("#loader").show();
});
</script>
@endpush
resources/views/auth/reset_password_form.blade.php
@extends("layouts.master")
@section("main-content")
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card">
<div class="card-header">
<h4>Reset Password</h4>
</div>
<div class="card-body">
@if (session()->has("error"))
<div class="alert alert-danger">
{{ session()->get("error") }}
</div>
@endif
<form action="{{ route('reset.password.post') }}" method="POST">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group mb-4">
<label for="email">Email</label>
<input type="text" class="form-control" name="email" autofocus value="{{ old('email') }}"
placeholder="Enter your Email">
<div class="error">
@error("email")
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mb-4">
<label for="password">New Password</label>
<input type="password" class="form-control" name="new_password"
placeholder="Enter New Password">
<div class="error">
@error("new_password")
{{ $message }}
@enderror
</div>
</div>
<div class="form-group mb-4">
<label for="password-confirm">Confirm
Password</label>
<input type="password" class="form-control" name="confirm_password"
placeholder="Re enter Password">
<div class="error">
@error("confirm_password")
{{ $message }}
@enderror
</div>
</div>
<div class="col-md-6 offset-md-4">
<button type="submit" class="submit-btn">
Reset Password
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
We will add some styles and the stylesheet is given below:
public/css/style.css
* {box-sizing: border-box;
}
body {
margin: 0;
font-family: Helvetica, sans-serif;;
}
h1,h4, h3 {
text-align: center;
margin-bottom: 20px;
margin-top: 10px;
}
footer{
text-align: center;
}
.container{
min-height:700px;
}
.custom-btn {
background-color: #2c3691;
color: #fff !important;
padding: 11px 16px !important;
border-radius: 7px;
}
.submit-btn {
background-color: #2c3691;
color: #fff !important;
padding: 5px 16px !important;
border-radius: 7px;
}
.error{
color: red;
}
li {
list-style: none;
font-family: var(--body-font);
}
a {
text-decoration: none;
}
ul {
margin: 0;
}
.navbar_inner nav ul li a {
color: var(--dark);
font-weight: 500;
font-size: 16px;
}
.navbar_inner nav ul li a.active {
color: var(--orange);
}
.navbar_inner nav ul li {
padding: 0px 10px;
}
.navbar_inner nav ul li:last-child {
padding-right: 0;
}
.navbar_section {
box-shadow: var(--shadow1);
position: relative;
}
.copyright {
position: relative;
text-align: center;
background-color: #11195a;
}
.copyright p {
padding: 0;
margin: 0;
color: #fff;
font-size: 14px;
padding: 10px;
}
#loader {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
background: rgba(0,0,0,0.75) url("/img/loader.gif") no-repeat center center;
z-index: 99999;
}
@media only screen and (max-width: 768px) {
.custom-btn {
background-color: var(--orange);
color: #fff !important;
padding: 8px 7px !important;
border-radius: 7px;
}
}
Test the application
From the project root, run the php development server by running the below artisan command:
php artisan serve
From the browser run localhost:8000
. Verify if the application is working correctly. Test below cases:
- Register User
- Email verification
- Login
- Change Password
- Forgot Password
For emails, you can create a Mailtrap account and add the settings in your .env
file. You can read the topic How to send mail in Laravel using Mailtrap for Mailtrap account setup.
MAIL_MAILER=smtp
MAIL_HOST=sandbox.smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
Post a Comment