How to remake Laravel 5.7 Email Verification for Rest API?
Or is it worth doing everything from scratch?
This case works for me. Full project code here.
1) Redesigned VerificationController controller
Removed redirects and made response()->json(...) responses.
<?php
namespace App\Http\Controllers\API\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Http\Request;
use Illuminate\Auth\Events\Verified;
class VerificationController extends Controller
{
use VerifiesEmails;
/**
* Show the email verification notice.
*
*/
public function show()
{
//
}
/**
* Mark the authenticated user's email address as verified.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function verify(Request $request)
{
// ->route('id') gets route user id and getKey() gets current user id()
// do not forget that you must send Authorization header to get the user from the request
if ($request->route('id') == $request->user()->getKey() &&
$request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return response()->json('Email verified!');
// return redirect($this->redirectPath());
}
/**
* Resend the email verification notification.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function resend(Request $request)
{
if ($request->user()->hasVerifiedEmail()) {
return response()->json('User already have verified email!', 422);
// return redirect($this->redirectPath());
}
$request->user()->sendEmailVerificationNotification();
return response()->json('The notification has been resubmitted');
// return back()->with('resent', true);
}
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
}
2) Added my Notification:
I made it so that the link in the email message led to my frontend and contained a temporarySignedRoute link for the request.
use Illuminate\Auth\Notifications\VerifyEmail as VerifyEmailBase;
class VerifyEmail extends VerifyEmailBase
{
// use Queueable;
/**
* Get the verification URL for the given notifiable.
*
* #param mixed $notifiable
* #return string
*/
protected function verificationUrl($notifiable)
{
$prefix = config('frontend.url') . config('frontend.email_verify_url');
$temporarySignedURL = URL::temporarySignedRoute(
'verification.verify', Carbon::now()->addMinutes(60), ['id' => $notifiable->getKey()]
);
// I use urlencode to pass a link to my frontend.
return $prefix . urlencode($temporarySignedURL);
}
}
3) Added config frontend.php:
return [
'url' => env('FRONTEND_URL', 'http://localhost:8080'),
// path to my frontend page with query param queryURL(temporarySignedRoute URL)
'email_verify_url' => env('FRONTEND_EMAIL_VERIFY_URL', '/verify-email?queryURL='),
];
4) Added to User model:
use App\Notifications\VerifyEmail;
and
/**
* Send the email verification notification.
*
* #return void
*/
public function sendEmailVerificationNotification()
{
$this->notify(new VerifyEmail); // my notification
}
5) Added routes
The following routes are used in Laravel:
// Email Verification Routes...
Route::get('email/verify', 'Auth\VerificationController#show')->name('verification.notice');
Route::get('email/verify/{id}', 'Auth\VerificationController#verify')->name('verification.verify');
Route::get('email/resend', 'Auth\VerificationController#resend')->name('verification.resend');
They are added to the application if used Auth::routes();.
As far as I understand the email/verify route and its method in the controller are not needed for Rest API.
6) On my frontend page /verify-email(from frontend.php config) i make a request to the address contained in the parameter queryURL
The received URL looks like this:
"http://localhost:8000/api/email/verify/6?expires=1537122891&signature=0e439ae2d511f4a04723a09f23d439ca96e96be54f7af322544fb76e3b39dd32"
My request(with Authorization header):
await this.$get(queryURL) // typical get request
The code perfectly verify the email and I can catch the error if it has already been verified. Also I can successfully resend the message to the email.
Did I make a mistake somewhere? Also I will be grateful if you improve something.
I tried Илья Зеленько answer but I must modify VerificationController construct method as follow
public function __construct()
{
$this->middleware('auth')->except(['verify','resend']);
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
otherwise laravel need autentication to access verify and resend routes
Related
How can you overwrite the token message 'This password reset token is invalid.'
I've tried adding this into my ResetPasswordController but it still displays the default token message.
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* #return string
*/
public function redirectTo()
{
return config('user.redirect', route('user.dashboard'));
}
/**
* Get the password reset validation error messages.
*
* #return array
*/
protected function validationErrorMessages()
{
return [
'token' => 'This password reset token is invalid. Request a new password'
];
}
/**
* Display the password reset view for the given token.
*
* If no token is present, display the link request form.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showResetForm(Request $request)
{
$token = $request->route()->parameter('token');
return view('auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
}
I need to add a URL instead of just changing the text. Overwrite Error text for 'The password reset token is invalid' Laravel
I've discovered it's the $this->broker()->reset() which actually validates the token. Is the only way to overwrite the token message by fully overwrite this method?
/**
* Reset the given user's password.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
public function reset(Request $request)
{
$request->validate($this->rules(), $this->validationErrorMessages());
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$response = $this->broker()->reset(
$this->credentials($request), function ($user, $password) {
$this->resetPassword($user, $password);
}
);
// If the password was successfully reset, we will redirect the user back to
// the application's home authenticated view. If there is an error we can
// redirect them back to where they came from with their error message.
return $response == Password::PASSWORD_RESET
? $this->sendResetResponse($request, $response)
: $this->sendResetFailedResponse($request, $response);
}
You can override the sendResetFailedResponse method to change the message. The $response is passed to that method which is the translation key for that message which is passwords.token (See resources/lang/en/passwords.php).
/**
* Get the response for a failed password reset.
*
* #param \Illuminate\Http\Request $request
* #param string $response
* #return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendResetFailedResponse(Request $request, $response)
{
if ($request->wantsJson()) {
throw ValidationException::withMessages([
'email' => ["Your custom error message here"],
]);
}
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => "Your custom error message here"]);
}
I have a question about my code for confirmation email guests. I have a comment system. When guest read a post, he can write a comment. When he add a comment, guest need confirm email (on comment form, I have field for email). How I can correctly do email confirmation? How In Laravel, when user register.
Now I have table "guests" and a model Guest, with columns: name, email, email_token and email_verified_at (how in users table).
In model guest I have function:
public function sendEmailNotification($token)
{
$this->notify((new ReviewEmailNotification($token)));
}
When comment is created, I call observed function created and send email:
public function created(Review $review)
{
Auth::guest() ? $review->sendEmailNotification($review->email_token) : '';
}
My notification:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Facades\Lang;
class ReviewEmailNotification extends Notification
{
use Queueable;
public $token;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct($token)
{
$this->token = $token;
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Get the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->subject(Lang::getFromJson('Verify comment'))
->line(Lang::getFromJson('Please confirm your Email:'))
->action(Lang::getFromJson('Confirm E-Mail'), url(config('app.url') . route('revemail', $this->token)))
->line(Lang::getFromJson('...'));
}
/**
* Get the array representation of the notification.
*
* #param mixed $notifiable
* #return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}
My function for confirm email:
public function activateEmail($token = null)
{
$guest = Guest::where('email_token', $token)->first();
if ($review) {
$guest->email_verified_at = now();
$guest->save();
}
return redirect()->route('home');
}
And when email is verified I create record on guests table.
This solution is working. But how I can go away from email_token? In users table I have only email_verified_at without column token. And I want do token with expire date, how in laravel. Can I use default laravel email confirmation for guests?
Simple modify your sendEmailNotification currently which is accepting $token change it to email and in your function where you are using token change it to email like this:
open this function ReviewEmailNotification
modify like this Review::where('email',$email)
want more help then post the complete code of ReviewEmailNotification
finally i understand your query, you have to change the code.... Dont use the user table for that because in every post/article will publish new comment/review so guest table will be good and add post_id, user_id for email confirmation , so your new function look like this
public function created(Review $review)
{
Auth::guest() ? $review->sendEmailNotification($review-> post_id,$review->user_id,) : '';
}
public function activateEmail($token = null)
{
$guest = Guest::where('post_id', $post_id)->where('user_id', $user_id)->first();
if ($review) {
$guest->email_verified_at = now();
$guest->save();
}
return redirect()->route('home');
}
public function sendEmailNotification($user_id,$post_id)
{
$this->notify((new ReviewEmailNotification($post_id,$user_id )));
}
I just updated my Laravel project from 5.6 to 5.7. The primary reason I upgraded was I needed to add Email Verification to my project. After I completed all upgrade steps and implemented the Email Verification as per the Laravel documentation I am getting an error. So the steps leading up to the error is this:
I used 1 route to test with, in my ..\routes\web.php file I have this line of code:
Route::get('dashboard', ['uses' => 'DashboardController#getDashboard'])->middleware('verified');
When I try to go to that route it does redirect me to the view for ..\views\auth\verify.blade.php as it should. There I click the link to send the verification email. I get the email then I click the button in the email to verify my email. It launches a browser and starts to navigate me somewhere and thats when it gets an error:
Class signed does not exist
After much research I discovered the error was in the new VerificationController.php file that the instructions said to create and the line of code causing the problem is:
$this->middleware('signed')->only('verify');
If I comment this line out and click the button in my email again then it works without any errors and my users email_verified_at column is updated with a datetime stamp.
Below is the entire VerificationController.pas in case it sheds any light on the problem:
<?php
namespace App\Http\Controllers\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
class VerificationController extends Controller
{
/*
|--------------------------------------------------------------------------
| Email Verification Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling email verification for any
| user that recently registered with the application. Emails may also
| be re-sent if the user didn't receive the original email message.
|
*/
use VerifiesEmails;
/**
* Where to redirect users after verification.
*
* #var string
*/
protected $redirectTo = '/dashboard';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
}
Take a look at the Laravel Documentation on Signed URLs
My guess is you are missing this entry in the $routeMiddleware array
// In app\Http\Kernel.php
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* #var array
*/
protected $routeMiddleware = [
...
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
];
I had same problem with API email verification and i had to add event that triggers the email sending in app/Providers/EventServiceProvider.php
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
and override app/Http/Controllers/Auth/VerificationController.php functions
/**
* Show the email verification notice.
*
*/
public function show()
{
}
/**
* Mark the authenticated user's email address as verified.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function verify(Request $request)
{
if ($request->route('id') == $request->user()->getKey() &&
$request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return response()->json('Email verified!');
}
/**
* Resend the email verification notification.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function resend(Request $request)
{
if ($request->user()->hasVerifiedEmail()) {
return response()->json('User already have verified email!', 422);
}
$request->user()->sendEmailVerificationNotification();
return response()->json('The notification has been resubmitted');
}
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
With the laravel 5.3 and above the concept of filters is gone and middleware is used instead. I have migrated my project with laravel 4 to 5.4.
I want to modify the DeviceLoginController that is when I am not logged in it must refresh to the login page. Other details can be seen in the controller page.
Problem: The controller page is useless as even when I am not logged in anyone can access this page and and anyone can fill anything. I have been trying to resolve this issue from 2 days still I am no where.
DeviceLoginController page looks like this:
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\BaseController;
use Auth;
use Format;
use Input;
use DB;
use Session;
use Validator;
use Hash;
use Redirect;
use User;
use App\Models\License;
use App\Models\LicenseCount;
use App\Models\Manufacturer;
use App\Models\DeviceModel as Model;
use App\Models\Device;
use App\Models\Application;
class DeviceLoginController extends BaseController {
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth');
}
public function attempt()
{
$username = Format::ltr(Input::get("username"));
$device_key = Input::get("device_key");
$imei = Format::ltr(Input::get('imei'));
$model = Format::utr(Input::get('model'));
$manufacturer = Format::utr(Input::get('manufacturer'));
$app_code = Format::ltr(Input::get('app_code'));
$user = User::where('username', $username)->first();
if(!Hash::check($device_key, $user->device_key)) {
Event::fire('auth.login.fail', array($username, Request::getClientIp(), time()));
die("1");
}
Auth::loginUsingId($user->id);
// check if device is already registered under given user for given app
$license = License::where('device_imei', $imei)->where('app_code', $app_code)->where('user_username', $username);
// if device isn't registered, first check if device is registered by different user. If not, check if licenses are available or not with the user to register new device
if(!$license->count()) {
// checking if licenses are available or not
$license_count = LicenseCount::where('user_username', $username)->where('app_code', $app_code)->first();
// if licenses are left, register the device
if((int) $license_count['left']) {
$manufacturer = Manufacturer::firstOrCreate(array('name' => $manufacturer));
$model = Model::firstOrCreate(array('name' => $model, 'manufacturer_code' => $manufacturer->code));
$device = Device::where('imei', $imei)->first();
if(!$device) {
$device = Device::firstOrCreate(array('imei' => $imei, 'model_code' => $model->code));
}
License::create(array('device_imei' => $imei, 'app_code' => $app_code, "user_username" => $username, "expiry_date" => date("Y-m-d H:i:s", strtotime("+1 year"))));
$license_count->left = Format::itr($license_count->left) - 1;
$license_count->save();
} else {
// Prints 3, if the device is not registered and user has no more licenses left for the given app
die("3");
}
// Prints 2, if the device was not previously registered and it is now registered under given user for given app
Session::put('login_response', '2');
} else {
// Prints 0, if device is already registered under given user for given app
Session::put('login_response', '0');
}
}
}
My authenticate.php file looks like this
<?php
namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;
class Authenticate
{
/**
* The authentication factory instance.
*
* #var \Illuminate\Contracts\Auth\Factory
*/
protected $auth;
/**
* Create a new middleware instance.
*
* #param \Illuminate\Contracts\Auth\Factory $auth
* #return void
*/
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string[] ...$guards
* #return mixed
*
* #throws \Illuminate\Auth\AuthenticationException
*/
public function handle($request, Closure $next, $guards=null)
{
if($this->auth ->guest())
{
if($request->ajax())
{
return response('unauthorized',401);
}
else
{
return redirect()->guest('login');
}
}
//$this->authenticate($guards);
return $next($request);
}
/**
* Determine if the user is logged in to any of the given guards.
*
* #param array $guards
* #return void
*
* #throws \Illuminate\Auth\AuthenticationException
*/
protected function authenticate(array $guards)
{
if (empty($guards)) {
return $this->auth->authenticate();
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
throw new AuthenticationException('Unauthenticated.', $guards);
}
}
I am new to Laravel please forgive me if I have asked some silly question. I am clueless what to do at this point. Please help and let me know if I need to add some other file.
It's great you have done the migration to Laravel 5.4. However, I suggest you go through the documentation first or watch the Laravel 5.4 from Scratch series.
For your question, you need the put the route that calls the controller function under the 'auth' middleware. Laravel provides this middleware out of the box. You can change the route to where the user will be redirected if he is not logged and calls the route.
Please go through the documentation for this.
Suppose your route is 'admin/profile' and you have defined this in the web.php routes file, you can add a middleware to it as shown (picked this example from the DOC.)
Route::get('admin/profile', function () {
//
})->middleware('auth');
To place multiple routes under the same middleware, you can use Route groups.
I'm using dingo/api package.
Controller:
public function register(RegisterUserRequest $request)
{
dd('a');
}
And for example the email field is required:
<?php namespace App\Http\Requests;
class RegisterUserRequest extends Request
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
];
}
}
So I send a request without the email, and still getting the "a" response.
I also tried to extend Dingo\Api\Http\Request instead of App\Http\Request, but still the same.
For Dingo to work at all with the FormRequest, by experience (and from this Issue), you have to use Dingo's Form request i.e Dingo\Api\Http\FormRequest; , so you'll have something similar to:
<?
namespace App\Http\Requests;
use Dingo\Api\Http\FormRequest;
use Symfony\Component\HttpKernel\Exception\HttpException;
class RegisterUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
];
}
// In case you need to customize the authorization response
// although it should give a general '403 Forbidden' error message
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof \Dingo\Api\Http\Request) {
throw new HttpException(403, 'You cannot access this resource'); //not a user?
}
}
}
PS: This is tested on Laravel 5.2.*
Hope it helps :)
According to the Wiki
you must overload the failedValidation and failedAuthorization methods.
These methods must throw one of the above mentioned exceptions and not the response HTTP exceptions that Laravel throws.
If you take a look at Dingo\Api\Http\FormRequest.php, you'll see:
class FormRequest extends IlluminateFormRequest
{
/**
* Handle a failed validation attempt.
*
* #param \Illuminate\Contracts\Validation\Validator $validator
*
* #return mixed
*/
protected function failedValidation(Validator $validator)
{
if ($this->container['request'] instanceof Request) {
throw new ValidationHttpException($validator->errors());
}
parent::failedValidation($validator);
}
/**
* Handle a failed authorization attempt.
*
* #return mixed
*/
protected function failedAuthorization()
{
if ($this->container['request'] instanceof Request) {
throw new HttpException(403);
}
parent::failedAuthorization();
}
}
Hence, you need to change the names of your methods appropriately, and have them throw the appropriate exceptions, instead of returning a boolean.
you need to call the validate function explicitly when you run it under an Dingo API setup, try something like this (for L5.2):
Probably a few extra providers
...
Illuminate\Validation\ValidationServiceProvider::class,
Dingo\Api\Provider\LaravelServiceProvider::class,
...
Aliases
...
'Validator' => Illuminate\Support\Facades\Validator::class,
...
I'm also pretty much sure that you really don't want to use this below as suggested here and there, It will expect form(encoded) input and will also probably fail on CSRF token as it expects it, so it will fail right after validating (form input). But make sure to test behavior with this on/off.
use Dingo\Api\Http\FormRequest;
Make your headers:
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use App\Http\Requests;
use App\Http\Controllers\Controller;
use Dingo\Api\Exception\ValidationHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/* This can be a tricky one, if you haven't split up your
dingo api from the http endpoint, there are plenty
of validators around in laravel package
*/
use Validator;
Then the actual code (if you adhere to cors standard,
this should be a POST and that commonly translates to a store request)
...
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function register(RegisterUserRequest $request) {
$validator = Validator::make($request->all(), $this->rules());
if ($validator->fails()) {
$reply = $validator->messages();
return response()->json($reply,428);
};
dd('OK!');
};
...
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required'
// or/and 'userid' => 'required'
];
}
That will give you back the response you expect from the validator. If you use this with pregenerated forms, it does not need this fix, there the validator will kick in automatically. (not under Dingo Api).
you probably also need these in composer.json
"dingo/api": "1.0.*#dev",
"barryvdh/laravel-cors": "^0.7.1",
This is untested, by heart, it took me 2 days to figure this out but I have a separate namespace for API specific and authenticated with middleware. success