Verificación por SMS en Laravel con Twilio

Share on facebook
Facebook
Share on twitter
Twitter
Share on email
Email

Iniciaremos asumiendo que ya esta funcionando la autenticación de usuarios como lo explicamos anteriormente.

La verificación por SMS requiere mas pasos que la verificación por correo pero no te preocupes que implementarla es muy sencillo.

  1. Cuenta en Twilio
  2. Projecto Laravel
  3. Migración
  4. Modelo, Vista, Controlador
  5. Rutas

Cuenta en Twilio

  • Registarse en Twilio.
  • Solicita un numero de prueba.
  • Busca tu ACCOUNT SID, AUTH TOKEN, PHONE

Proyecto Laravel

  • Agrega el paquete de Twilio usando Composer
composer require twilio/sdk
  • Agregue el paquete de Guzzle para los llamados a la API de Twilio
composer require guzzlehttp/guzzle
  • Agregue el siguiente código en config/services.php para acceder a las variables de configuración Twilio globalmente.
    'twilio' => [
       'account_sid' => env('TWILIO_ACCOUNT_SID'),
       'auth_token' => env('TWILIO_AUTH_TOKEN'),
       'phone' => env('TWILIO_PHONE'),
    ],
  • Agregue estas opciones de configuración de Twilio en su archivo .env con los datos obtenidos cuando registro su cuenta en Twilio.
TWILIO_ACCOUNT_SID=ACe373356b19aa3c74550427ba40b3f6a9
TWILIO_AUTH_TOKEN=f275663524fbb6169ec38e01c02c14dd
TWILIO_PHONE=+15088154742

Migración

  • Cree la migración utilizando el comando Artisan make:migration
php artisan make:migration add_sms_verification_to_users_table
  • En el archivo generado de la migración agregue las columnas phone, verification_code, phone_verified_at, utilizando el código.
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class AddSmsVerificationToUsersTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->string('phone');
            $table->string('verification_code')->nullable();
            $table->timestamp('phone_verified_at')->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            //
        });
    }
}
  • Migrar las tablas
php artisan migrate

Modelo, Vista, Controlador

  • Vamos a editar el modelo user app/User.php.
<?php

namespace App;

use App\Http\Controllers\Twilio;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'name', 'email', 'phone', 'password',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password', 'remember_token',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
        'phone_verified_at' => 'datetime',
    ];

    /**
     * Determine if the user has verified their phone.
     *
     * @return bool
     */
    public function hasVerifiedPhone()
    {
        return ! is_null($this->phone_verified_at);
    }

    /**
     * Mark the given user's phone as verified.
     *
     * @return bool
     */
    public function markPhoneAsVerified()
    {
        return $this->forceFill(['verification_code' => $this->null, 'phone_verified_at' => $this->freshTimestamp(),])->save();
    }
    
    /**
     * Send the sms verification code.
     *
     * @return void
     */
    public function sendPhoneVerificationNotification()
    {
        $verification_code = rand(100000,999999);
        Twilio::sendMessage($verification_code, $this->phone);
        return $this->forceFill(['verification_code' => $verification_code, 'phone_verified_at' => $this->null,])->save();
    }
}
  • Modificamos la vista de registro resources/views/auth/register.blade.php.
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Register') }}</div>

                <div class="card-body">
                    <form method="POST" action="{{ route('register') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>

                            <div class="col-md-6">
                                <input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>

                                @error('name')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>

                            <div class="col-md-6">
                                <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email">

                                @error('email')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="phone" class="col-md-4 col-form-label text-md-right">{{ __('Phone') }} {{ __('(International Format)') }}</label>

                            <div class="col-md-6">
                                <input id="phone" type="text" class="form-control @error('phone') is-invalid @enderror" name="phone" value="{{ old('phone') }}" required autocomplete="phone">

                                @error('phone')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>

                            <div class="col-md-6">
                                <input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">

                                @error('password')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row">
                            <label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>

                            <div class="col-md-6">
                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-6 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Register') }}
                                </button>
                            </div>
                        </div>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
  • Creamos la vista verify_phone.blade.php en la carpeta auth y agregamos el siguiente código.
@extends('layouts.app')

@section('content')
<div class="container">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">{{ __('Verify Your Phone') }}</div>

                <div class="card-body">
                    @if (session('resent'))
                        <div class="alert alert-success" role="alert">
                            {{ __('A fresh verification code has been sent to your phone.') }}
                        </div>
                    @endif

                    <form method="POST" action="{{ route('verification-phone.verify') }}">
                        @csrf

                        <div class="form-group row">
                            <label for="code" class="col-md-4 col-form-label text-md-right">{{ __('Code') }}</label>

                            <div class="col-md-6">
                                <input id="code" type="text" class="form-control @error('code') is-invalid @enderror" name="code" value="{{ old('code') }}" required autocomplete="code" autofocus>

                                @error('code')
                                    <span class="invalid-feedback" role="alert">
                                        <strong>{{ $message }}</strong>
                                    </span>
                                @enderror
                            </div>
                        </div>

                        <div class="form-group row mb-0">
                            <div class="col-md-8 offset-md-4">
                                <button type="submit" class="btn btn-primary">
                                    {{ __('Verify') }}
                                </button>
                            </div>
                        </div>
                    </form>

                    {{ __('Before proceeding, please check your phone for a verification code.') }}
                    {{ __('If you did not receive the code') }},
                    <a class="btn btn-link p-0 m-0 align-baseline" href="{{ route('verification-phone.resend') }}">{{ __('click here to request another') }}</a>
                </div>
            </div>
        </div>
    </div>
</div>
@endsection
  • Creamos el controlador Twilio.
php artisan make:controller Twilio
  • Agregamos el siguiente código al controlador creado en app/Controllers/Twilio.php.
<?php

namespace App\Http\Controllers;

use Twilio\Rest\Client;
use Illuminate\Http\Request;

class Twilio extends Controller
{
    /**
     * Sends sms to user using Twilio's programmable sms client
     * @param String $message Body of sms
     * @param Number $recipients string or array of phone number of recepient
     */
    public static function sendMessage($message, $recipients)
    {
        $account_sid = config('services.twilio')['account_sid'];
        $auth_token = config('services.twilio')['auth_token'];
        $phone = config('services.twilio')['phone'];
        $twilio = new Client($account_sid, $auth_token);
        $twilio->messages->create($recipients, [
            'from' => $phone,
            'body' => $message
        ] );
    }
}
  • Creamos el controlador VerificationPhoneController
php artisan make:controller Auth/VerificationPhoneController
  • Agregamos el siguiente código al controlador creado en app/Controllers/Auth/VerificationPhoneController.php.
<?php

namespace App\Http\Controllers\Auth;

use App\User;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class VerificationPhoneController extends Controller
{
    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('auth');
    }

    /**
     * Show form to verify code.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function show(Request $request)
    {
        if ($request->user()->hasVerifiedPhone()) {
            return redirect()->route('home');
        }

        return view('auth/verify_phone');
    }

    /**
     * Mark the authenticated user's phone as verified.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function verify(Request $request)
    {
        if ($request->user()->hasVerifiedPhone()) {
            return redirect()->route('home');
        }

        $request->validate(['code' => ['required', 'string', 'min:6', 'max:6', 'regex:/([0-9]{6,6})/'],]);
        
        if ($request->user()->verification_code !== $request->code)
        {
            return back()->withErrors(['code' => __('The code your provided is wrong. Please try again or request another code.')]);
        }
        
        $request->user()->markPhoneAsVerified();
        
        return redirect()->route('home')->with('status', __('Your phone was successfully verified.'));
    }
    
    /**
     * Resend the phone code.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function resend(Request $request)
    {
        if ($request->user()->hasVerifiedPhone()) {
            return redirect()->route('home');
        }

        $request->user()->sendPhoneVerificationNotification();

        return back()->with('resent', true);
    }
}
  • Modificamos el controlador app/Http/Controllers/Auth/RegisterController.php.
<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;

class RegisterController extends Controller
{
    /*
    |--------------------------------------------------------------------------
    | Register Controller
    |--------------------------------------------------------------------------
    |
    | This controller handles the registration of new users as well as their
    | validation and creation. By default this controller uses a trait to
    | provide this functionality without requiring any additional code.
    |
    */

    use RegistersUsers;

    /**
     * Where to redirect users after registration.
     *
     * @var string
     */
    protected $redirectTo = '/home';

    /**
     * Create a new controller instance.
     *
     * @return void
     */
    public function __construct()
    {
        $this->middleware('guest');
    }

    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array  $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
            'phone' => ['required', 'string', 'min:8', 'max:16', 'regex:/([+][0-9]{8,16})/'],
            'password' => ['required', 'string', 'min:8', 'confirmed'],
        ]);
    }

    /**
     * Create a new user instance after a valid registration.
     *
     * @param  array  $data
     * @return \App\User
     */
    protected function create(array $data)
    {
        $user = User::create([
            'name' => $data['name'],
            'email' => $data['email'],
            'phone' => $data['phone'],
            'password' => Hash::make($data['password']),
        ]);
        
        $user->sendPhoneVerificationNotification();

        return $user;
    }
}

Rutas

  • Agregamos las rutas al routes/web.php.
Route::get('phone/verify', 'Auth\VerificationPhoneController@show')->name('verification-phone.show');
Route::post('phone/verify', 'Auth\VerificationPhoneController@verify')->name('verification-phone.verify');
Route::get('phone/resend', 'Auth\VerificationPhoneController@resend')->name('verification-phone.resend');

Podría Interesarte

Crear paquete para Laravel con composer

Una de las ventajas de Laravel es que podemos escribir bibliotecas donde simplemente podemos llamar a sus servicios sin preocuparnos de cómo se implementó. Podemos reutilizar estas bibliotecas y administrarlas por separado del código fuente principal de nuestro proyecto y a estas bibliotecas en Laravel se les llaman paquetes.

Verificación por Correo en Laravel

Verificar que el usuario es una persona real cada día es mas necesario y la verificación por correo nos brinda cierta seguridad de esto.