Laravel Email Verification From API is not Working - php

I'm trying to verify the email with API but I always get "Invalid/Expired url provided.".
From the email I got a verification URL like
http://localhost:8000/email/verify/9/36be72d64a3dfec027a14bc40cea3e76424dea05?expires=1620495480&signature=59aef4e96a56e4fa8e6ebcdf3c9e3ebef2eac5f21190c6baa8ae30d17e2fbee9
and on postman I try to send a request like below I get invalid signature error
http://localhost:8000/api/verify-email/9?expires=1620495480&hash=36be72d64a3dfec027a14bc40cea3e76424dea05&signature=59aef4e96a56e4fa8e6ebcdf3c9e3ebef2eac5f21190c6baa8ae30d17e2fbee9
Routes
Route::get( '/verify-email/{id}', [
\App\Http\Controllers\ApiAuth\VerifyEmailController::class,
'index'
] )->name( 'verification.verify' );
Controller
public function index($user_id, Request $request) {
if (!$request->hasValidSignature()) {
return response(["success"=> false, "message" => "Invalid/Expired url provided."], 401);
}
$user = User::find($user_id);
if(!$user) {
return response(["success"=> false, "message" => "Invalid User"], 401);
}
if (!$user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
return response(["success"=> true] );
}else {
return response(["success"=> true, "message" => "Email Already Verified"] );
}
}

Related

Laravel response also contain request payload

when I make request for forget user password api
POST /api/forget-password
Route::post('forget-password', [UserApiController::class, 'forgetPassword']);
Sample Request
{
"email": "example#gmail.com"
}
Expected response
{ "message": "success"}
Actual response what i getting now is
{"email": "example#gmail.com"}{"message": "success"}
Controller
public function forgetPassword(Request $request)
{
$user = User::firstWhere('email', $request->email);
if ($user) {
$auto_pwd = Str::random(8);
$hashed_random_password = Hash::make($auto_pwd);
$user->update([
'password' => $hashed_random_password,
]);
$this->sendUserCreationEmail($user, $auto_pwd);
return $this->respondCreateMessageOnly('success');
} else {
return $this->respondErrorToken('Enter Correct Email');
}
}
public function respondCreateMessageOnly($message)
{
return response()->json([
// 'code' => Response::HTTP_OK,
'message' => $message,
], 200);
}
here is the controller of that route
Laravel version - Laravel Framework 8.8.0

Laravel 7: Verify email using API endpoint + single page application

I have a NuxtJS/Vue SPA and I want to verify the user email with the Laravel API that's my server side.
I create a custom notification called VerifyEmail.php:
<?php
namespace App\Notifications;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Lang;
use Illuminate\Notifications\Notification;
class VerifyEmail extends Notification {
public function via($notifiable) {
return ['mail'];
}
public function toMail($notifiable) {
$params = [
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
'expiry' => Carbon::now()->addMinutes(60)->timestamp
];
$url = config('app.web_client_url') . '/verify-email?';
foreach ($params as $key => $param) {
$url .= "{$key}={$param}&";
}
$key = config('app.key');
$signature = hash_hmac('sha256', $url, $key);
$url .= "signature=$signature";
return (new MailMessage)
->subject(Lang::get('Verify Email Address'))
->line(Lang::get('Please click the button below to verify your email address.'))
->action(Lang::get('Verify Email Address'), $url)
->line(Lang::get('If you did not create an account, no further action is required.'));
}
}
In my registration controller when a user registers I use:
...
$user->save();
$user->notify(new VerifyEmail());
return response()->json([
'message' => $user
], 201);
and the email gets sent. The URL in the email is something like: https://localhost:7000/verify-email?id=37&hash=4c1691e6db623b85d90cee62f80d6f9085648c92&expiry=1595596017&signature=d6c6374b203b1da66d11818728921a4160e30ebf43c5a8be544220c8eca97bb3 (localhost:7000 is the address of my NuxtJS application).
Upon going to that page, I make the following request in the mounted lifecycle method:
this.signature = this.$route.query.signature
this.expiry = this.$route.query.expiry
this.hash = this.$route.query.hash
this.id = this.$route.query.id
this.$axios.$get(`api/email/verify/${this.id}?hash=${this.hash}&expiry=${this.expiry}&signature=${this.signature}`)
.then(response => {
this.successMessage = response
}).catch(error => {
this.errorMessage = error
})
This request hits the endpoint on my server and the following method runs:
public function verify($user_id, Request $request) {
if (!$request->hasValidSignature()) { // Check always fails and we get a 401
return response()->json(["msg" => "Invalid URL provided."], 401);
}
$user = User::findOrFail($user_id);
if (!$user->hasVerifiedEmail()) {
$user->markEmailAsVerified();
}
return response()->json(["msg" => "Email verified."], 200);
}
The route for laravel endpoint:
Route::get('email/verify/{id}', 'Api\EmailVerificationController#verify')->name('verification.verify');
I can see that the parameters are received in the verify method request object parameter (e.g. setting a breakpoint and checking):
The check for a valid signature always fails and results in a 401 being sent back to the client. What's wrong with the URL/signature that I'm generating?
Here what I did to solve the problem. Go to AuthServiceProvider
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
//
VerifyEmail::createUrlUsing(function ($notifiable) {
$params = [
"expires" => Carbon::now()
->addMinutes(60)
->getTimestamp(),
"id" => $notifiable->getKey(),
"hash" => sha1($notifiable->getEmailForVerification()),
];
ksort($params);
// then create API url for verification. my API have `/api` prefix,
// so i don't want to show that url to users
$url = \URL::route("verification.verify", $params, true);
// get APP_KEY from config and create signature
$key = config("app.key");
$signature = hash_hmac("sha256", $url, $key);
// generate url for yous SPA page to send it to user
return env("APP_FRONT") .
"/auth/verify-email/" .
$params["id"] .
"/" .
$params["hash"] .
"?expires=" .
$params["expires"] .
"&signature=" .
$signature;
});
}
}
add this to api.php
Route::get("/verify-email/{id}/{hash}", [
VerifyEmailController::class,
"__invoke",
])
->middleware(["auth:sanctum","signed", "throttle:6,1"])
->name("verification.verify");
add this to VerifyEmailController.php
/**
* Mark the authenticated user's email address as verified.
*
* #param \Illuminate\Foundation\Auth\EmailVerificationRequest $request
* #return \Illuminate\Http\RedirectResponse
*/
public function __invoke(EmailVerificationRequest $request)
{
if ($request->user()->hasVerifiedEmail()) {
return response()->json(
[
"message" => "Your'r email already verified.",
],
Response::HTTP_BAD_REQUEST
);
}
if ($request->user()->markEmailAsVerified()) {
event(new Verified($request->user()));
}
return response()->json(
[
"message" => "Verification complete thank you.",
],
Response::HTTP_OK
);
}
}
Front end
async verfyEmail() {
try {
const params = new URLSearchParams(this.$route.query)
let res = await this.$axios.get(
'verify-email/' +
this.$route.params.id +
'/' +
this.$route.params.hash,
{ params }
)
this.$router.push({ name: 'platform-dashboard' })
} catch (error) {
console.log(error.response)
this.$router.push({ name: 'platform-dashboard' })
}
}

Is there any way to change response of auth:api , A laravel api_token

I am creating API with Default api-authentication
I am using laravel 6.x
Its run when i generate on user register and pass generated token with request.
But
when i pass a wrong token, Then it shows a Login page HTML, i want to show some custom JSON response instead of HTML
Also is there any way to check that passed token is same with passed user id or not. Because user can pass different user id with token.
My api route file as below
Route::middleware('auth:api')->post('/listUser', 'ApiController#listUser');
I have manage my points as below
For Point 1
when i pass a wrong token, Then it shows a Login page HTML, i want to show some custom JSON response instead of HTML
I made change in App/Exceptions/handler.php
Modify render function as below
public function render($request, Exception $exception)
{
if ($exception instanceof NotFoundHttpException) {
if ($request->is('api/*')) {
return response()->json(['error' => 'Not Found'], 404);
}
//return response()->view('404', [], 404);
}
return parent::render($request, $exception);
}
It workrs well because i have an api based routes
My api route look likes
// Request with Authentication v1
Route::group(['prefix' => 'v1', 'namespace' => 'Api\v1', 'middleware' => ['api','auth:api'] ], function () {
Route::post('/myProfile', 'ApiController#myProfile');
});
// Request without Authentication v1
Route::group(['prefix' => 'v1', 'namespace' => 'Api\v1', 'middleware' => 'api'], function () {
Route::post('/register', 'ApiController#register');
});
For Point 2
Also is there any way to check that passed token is same with passed user id or not. Because user can pass different user id with token.
For that i have created a function checkValidations in ApiController and check user id is associated with particular token or not as below:
In that function i check in way that
Check for all validation passed from called method
Match token associated with user id then return success
else return invalid token response
Function Code
public function checkValidations($required = [], $request = [])
{
$validator = Validator::make($request->all(), $required);
if ($validator->fails()) {
$this->response[] = array(
'status' => 'false',
'response_msg' => implode(",",$validator->messages()->all()),
);
return array('response' => $this->response);
} else if(isset($request['api_token']) && auth('api')->user()->id ==
$request['id']) {
return 'success';
} else {
$this->response[] = array(
'status' => 'false',
'response_msg' => 'Invalid token',
);
return array('response' => $this->response);
}
}
And call that checkValidations from any function and can reuse it as
public function myProfile(Request $request)
{
$validation = [
'id' => 'bail|required|exists:users',
'api_token' => 'bail|required|min:60|max:60'
];
if( $this->checkValidations($validation, $request) == 'success'){
$this->response[] = array(
'status' => 'true',
'response_msg' => 'Success',
'data' => auth('api')->user()
);
}
return array('response' => $this->response);
}
May be there is many other best way to manage that points, but i didn't found, so i manage in above ways.
You can configure a custom response in the Authenticate middleware. e.g.
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->guest()) {
if ($guard === 'api') {
return response('Unauthorized.', 401);
} else {
return redirect()->guest('login');
}
}
return $next($request);
}
You can do this by extending the TokenGuard, with your custom logic. Or you can create a new Middleware, which asserts that user authenticated by API matches the passed user ID.
I just verified the kind of exception if is related with authentication and then the URL( as API guard use '/api' just verify it) and fire the response.
if($exception instanceof \Illuminate\Auth\AuthenticationException){
if($request->is('api/*')){
return response()->json([
'success' => false,
'message' => 'User not logged'
]);
}
}
I made the below change in app/Exceptions/Handler.php.
protected function unauthenticated($request, AuthenticationException $exception)
{
if ($request->expectsJson()) {
return response()->json(['error' => 'Not Authorized'], 404);
}
return redirect()->guest(route('login'));
}
Add use Illuminate\Auth\AuthenticationException in the document. Also, do not forget to add X-Requested-With:XMLHttpRequest to your request header. (Or Headers in postman)
return redirect()->guest(route('login')); is to redirect you to login page when you are not using the APIs.

Return JSON response instead of 401 Blade file

I am using AuthBasic for API authentication in a Laravel project,
I have this problem: when the API request authentication is invalid instead of displaying the JSON response it returns the 401 default blade view template.
Here is the code:
app\Http\Middleware\AuthBasic.php
public function handle($request, Closure $next)
{
if (Auth::onceBasic()) {
return response()->json(["message", "Authentication Required!"], 401);
} else {
return $next($request);
}
}
Found the Solution:
app\Exceptions\Handler.php
public function render($request, Exception $exception)
{
if ($request->is('api/*') || $request->wantsJson())
{
$json = [
'success' => false,
'error' => [
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
],
];
return response()->json($json, 401);
}
return parent::render($request, $exception);
}
Remove the 401 or change it to 200 from this line:
return response()->json(["message", "Authentication Required!"], 401);
See the reference, the second parameter is defining the http code to send the browser. [401] in you case.
https://laravel.com/api/5.7/Illuminate/Routing/ResponseFactory.html#method_json
This will fix your problem, probably!
public function handle($request, Closure $next)
{
$result = Auth::onceBasic();
if($result === 401)
return response()->json(["message", "Authentication Required!"]);
else
return $next($request);
}
So here is a half Solution for this problem:
vendor\laravel\framework\src\Illuminate\Auth\SessionGuard.php
public function onceBasic($field = 'email', $extraConditions = [])
{
$credentials = $this->basicCredentials($this->getRequest(), $field);
if (! $this->once(array_merge($credentials, $extraConditions))) {
//return $this->failedBasicResponse();
return response()->json(["Message" => "Authentication Required!"], 401);
}
}
So Instead of returning the Failed Basic Response it will return the JSON Message, but I don't want to make changes in Laravel Core Files, because in case of update they will get lost !
So Any Idea ?

Laravel 5.7 Auth with email/username and password using tymon/jwt

I am using jwt-auth for my Laravel 5.7 app. Currently, I'm allowing the client to enter email and password as user credentials.
However, I also want to let the client to enter their username in place of their email. So they have 2 choices: email or username.
How can I do that in my code?
My UserController#authenticate
public function authenticate(Request $request)
{
$credentials = $request->only('email', 'password');
try {
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json([
'status' => 401,
'message' => 'invalid_credentials',
], 401);
}
} catch(JWTException $e) {
return response()->json([
'status' => 500,
'message' => 'token_creation_failed',
], 500);
}
return response()->json(compact('token'));
}
Thanks in advance
In your AuthController, add this to the login method;
public function login()
{
$loginField = request()->input('login');
$credentials = null;
if ($loginField !== null) {
$loginType = filter_var($loginField, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
request()->merge([ $loginType => $loginField ]);
$credentials = request([ $loginType, 'password' ]);
} else {
return $this->response->errorBadRequest('What do you think you\'re doing?');
}
if (! $token = auth()->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
This is how I handled mine where user can choose either email or phone to login.
public function login(Request $request)
{
//validate incoming request
$this->validate($request, [
'email_phone' => 'required|string',
'password' => 'required|string',
]);
try {
$login_type = filter_var( $request->email_phone, FILTER_VALIDATE_EMAIL ) ? 'email' : 'phone';
// return $login_type;
$credentials = [$login_type => $request->email_phone, 'password'=>$request->password];
if (! $token = Auth::attempt($credentials)) {
return response()->json($this->customResponse("failed", "Unauthorized"), 401);
}
return $this->respondWithToken($token);
} catch(JWTException $e) {
return response()->json($this->customResponse("failed", "An error occured, please contact support.", $user), 500);
}
}

Categories