Autheticate via Laravel Sanctum by passing token as a GET query parameter - php

I know the dangers of passing the token as a GET parameter. I've seen this and this. However, in my case there is no other way because the route will get called by a script which I have no influence on.
I think I should implement a custom Guard which extends the Illuminate\Auth\RequestGuard and override the public function user() method. What I don't understand is, where does $this->callback point to? dd says it's an instance of Laravel\Sanctum\Guard .. but which method?
Laravel\Sanctum\Guard {#265 ▼
#auth: Illuminate\Auth\AuthManager {#267 ▶}
#expiration: null
#provider: null
}

I had to let some requests with TOKEN URL to return a pdf content. So I created middleware to validate if a token exists and then add it in to the header response, in that way I took advantage of the "normal" sanctum token validation.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
/**
* This middleware check if the request has _token key and adds this into the Authorization header to take advantage of
* the sanctum middleware
*/
class CheckTokenAndAddToHeaderMiddleware
{
/**
* Handle an incoming request.
*
* #param Request $request
* #param Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* #return Response|RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
$all = $request->all();
if (isset($all['_token'])) {
Log::debug('token from http param', [$all['_token']]);
$request->headers->set('Authorization', sprintf('%s %s', 'Bearer', $all['_token']));
}
return $next($request);
}
}
Given my requirement, I decided to put this middleware over all the URLs, so I added it before all API calls (it could be different for you).
Kernel.php
/**
* The application's route middleware groups.
*
* #var array
*/
protected $middlewareGroups = [
'api' => [
CheckTokenAndAddToHeaderMiddleware::class,
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class
],
];
I hope this could be useful for someone.
Regards.

I have a solution now.. I ended up extending Laravel\Sanctum\Guard and registering a new Illuminate\Auth\RequestGuard with the custom Sanctum Guard.
Here is the result:
app/Services/Auth/CustomSanctumGuard.php
<?php
namespace App\Services\Auth;
use Arr;
use Illuminate\Http\Request;
use Laravel\Sanctum\Events\TokenAuthenticated;
use Laravel\Sanctum\Guard;
use Laravel\Sanctum\Sanctum;
use Laravel\Sanctum\TransientToken;
class CustomSanctumGuard extends Guard
{
/**
* Retrieve the authenticated user for the incoming request.
*
* #param \Illuminate\Http\Request $request
* #return mixed
*/
public function __invoke(Request $request)
{
if ($token = $request->bearerToken() ?: $request->token) {
$model = Sanctum::$personalAccessTokenModel;
$accessToken = $model::findToken($token);
if (! $this->isValidAccessToken($accessToken) ||
! $this->supportsTokens($accessToken->tokenable)) {
return;
}
$tokenable = $accessToken->tokenable->withAccessToken(
$accessToken
);
event(new TokenAuthenticated($accessToken));
if (method_exists($accessToken->getConnection(), 'hasModifiedRecords') &&
method_exists($accessToken->getConnection(), 'setRecordModificationState')) {
tap($accessToken->getConnection()->hasModifiedRecords(), function ($hasModifiedRecords) use ($accessToken) {
$accessToken->forceFill(['last_used_at' => now()])->save();
$accessToken->getConnection()->setRecordModificationState($hasModifiedRecords);
});
} else {
$accessToken->forceFill(['last_used_at' => now()])->save();
}
return $tokenable;
}
}
}
app/Providers/AuthServiceProvider.php
<?php
namespace App\Providers;
use Auth;
use Illuminate\Auth\RequestGuard;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use App\Services\Auth\CustomSanctumGuard;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
// 'App\Models\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
Auth::resolved(function ($auth) {
$auth->extend('custom', function ($app, $name, array $config) use ($auth) {
return new RequestGuard(
new CustomSanctumGuard($auth, config('sanctum.expiration'), $config['provider']),
request(),
$auth->createUserProvider($config['provider'] ?? null)
);
});
});
}
}
config/auth.php
<?php
return [
// ...
'guards' => [
'custom' => [
'driver' => 'custom',
'provider' => 'users',
],
// ...
],
// ...
];
config/sanctum.php
<?php
return [
// ...
'guard' => ['custom'],
// ...
];

Heres my version of the middleware that will look for a token in the URL and attach it to the request as an authorization header. Does not do anything if this header is already present.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class AddSanctumTokenToHeaders
{
public function handle(Request $request, Closure $next)
{
// If the URL contains a token parameter - attach it as the Authorization header
if ($request->has('token') && !$request->headers->has('Authorization')) {
$request->headers->set('Authorization', 'Bearer ' . $request->token);
}
return $next($request);
}
}
Then register this middleware in your api route group (or any group you want this action to apply to)
<?php
protected $middlewareGroups = [
'api' => [
\App\Http\Middleware\AddSanctumTokenToHeaders::class,
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class
],
];

You can add the below code in AppServiceProvider boot method to authenticate using the token as query param. Laravel sanctum accepts the callable function which overrides the default. You can modify the code the way it fits for your application.
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
Sanctum::getAccessTokenFromRequestUsing(
function ($request) {
return $request->token;
}
);
}

Related

How to prevent a Laravel security leak where an API returns not found when you are not authenticated

Lets say I have the following route to display a specific user in an API point, that is protected via some authentication middle:
Route::get('/v1/user/{user}', 'Api\V1\UserController#show')->middleware('can:show,user');
Assume my database is filled with just a handfull records. Then, going to the route without a credential:
/api/v1/user/1 will give me 403 UNAUTHORIZED
/api/v1/user/999999 will give me 404 NOT FOUND
In this setup, any visitor can find out how many users I have, by just poking around and trying some routes. Moreover, they can also find out the primary identifier (the id) of these users. This is something I would like to hide from a business perspective, but also from a security perspective.
An appoach that partially addresses this issue, is using UUID's. UUIDs are universally unique alpha-numeric identifiers that are 36 characters long, and Laravel supports the use of these on your models. This will hide the amount of records our have, and make it hard to find existing records. However, since it is still statistically possible to find records by just brute forcing, I feel this is not the correct answer to this problem.
So, how can I prevent a Laravel security leak where API returns not found when you are not authenticated?
// Path: app/Http/Controllers/Api/V1/UserController.php
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Http\Request;
class UserController extends Controller
{
public function show(User $user)
{
return $user;
}
}
// Path: app/User.php
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
// Path: routes/api.php
<?php
Route::get('/v1/user/{user}', 'Api\V1\UserController#show')->middleware('can:show,user');
// Path: app/Providers/AuthServiceProvider.php
<?php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* #var array
*/
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
/**
* Register any authentication / authorization services.
*
* #return void
*/
public function boot()
{
$this->registerPolicies();
//
}
}
// Path: app/Http/Middleware/Can.php
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\Access\Response;
use Illuminate\Support\Facades\Gate;
class Can
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #param string $ability
* #param array|string $arguments
* #return mixed
*
* #throws \Illuminate\Auth\Access\AuthorizationException
*/
public function handle($request, Closure $next, $ability, ...$arguments)
{
if (Gate::denies($ability, $arguments)) {
throw new AuthorizationException(
'This action is unauthorized.', null, Response::deny()
);
}
return $next($request);
}
}
// Path: app/Http/Kernel.php
<?php
namespace App\Http;
use App\Http\Middleware\Can;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* #var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
];
/**
* The application's route middleware groups.
*
* #var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'bindings',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* #var array
*/
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => Can::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
// Path: app/Http/Controllers/Api/V1/UserControllerTest.php
<?php
namespace Tests\Feature\Api\V1;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
use RefreshDatabase;
public function testShow()
{
$user = factory(User::class)->create();
$response = $this->getJson("/api/v1/user/{$user->id}");
$response->assertStatus(200);
}
public function testShowUnauthorized()
{
$user = factory(User::class)->create();
$response = $this->getJson("/api/v1/user/{$user->id}");
$response->assertStatus(403);
}
public function testShowUnauthorizedWithPolicy()
{
$user = factory(User::class)->create();
$response = $this->getJson("/api/v1/user/{$user->id}");
$response->assertStatus(403);
}
}
// Path: app/Policies/UserPolicy.php
<?php
namespace App\Policies;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view the model.
*
* #param \App\User $user
* #param \App\User $model
* #return mixed
*/
public function show(User $user, User $model)
{
return $user->id === $model->id;
}
}
You can follow GitHub's technique and return a 404 (Not found) instead of 403 (Unauthorized).
This way, attackers don't know if the resource actually exists or not.
To achieve this in Laravel, you may do it like this:
In app/Exceptions/Handler.php, create/edit the method called render() and check if the status code is 403. If so, throw a 404 instead.
public function render($request, Throwable $exception)
{
if ($exception->getStatusCode() === 403) {
abort(404);
}
return parent::render($request, $exception);
}
If you want to test it, just add this test route in routes/web.php:
Route::get('/test', function () {
return abort(403); // should return a 404
});
Resources:
Related topic's discussion
Method render documentation
you can use this package to generate a slug.
Then you can use the slug instead of the id.
https://github.com/spatie/laravel-sluggable

Laravel - How to override the request class so that a script is executed whenever an http request is made to the backend?

I have managed this by extending the Form request like this:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Http\Controllers\Root\RootController;
use App\Http\Controllers\Root\ConfigRootController;
class Request 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 [
//
];
}
/**
* Intercept the request and make changes
*
*/
protected function getValidatorInstance()
{
$data = $this->all();
RootController::setConn($data['url']);
$Config = ConfigRootController::get_tl_config($data['url']);
if(isset($data['cookie'])){
$User = ConfigRootController::getCurrentUser($data['url'],$data['cookie']);
$GLOBALS['user']=$User;
}
$GLOBALS['CONFIG'] = json_decode($Config->getBody()->getContents());
return parent::getValidatorInstance();
}
}
However this solution has a limitation as it does not allow me to use the $request->validate() method in the controller, which is a requirement.
Given that I am using laravel as a stateless API. I need to intercept all requests made and inject the script below right before the controller so that I can access and handle the config data as needed.
$data = $this->all();
RootController::setConn($data['url']);
$Config = ConfigRootController::get_tl_config($data['url']);
if(isset($data['cookie'])){
$User = ConfigRootController::getCurrentUser($data['url'],$data['cookie']);
$GLOBALS['user']=$User;
}
$GLOBALS['CONFIG'] = json_decode($Config->getBody()->getContents());
Here is an attempt via extending the Request class and applying the script in a constructor like so :
namespace App\Http\Requests;
// use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http;
use App\Http\Controllers\Root\RootController;
use App\Http\Controllers\Root\ConfigRootController;
class Request extends Http\Request
{
public function __construct()
{
$data = $this->all();
RootController::setConn($data['url']);
$Config = ConfigRootController::get_tl_config($data['url']);
if(isset($data['cookie'])){
$User = ConfigRootController::getCurrentUser($data['url'],$data['cookie']);
$GLOBALS['user']= json_decode($User->getBody()->getContents());
}
$GLOBALS['TL_CONFIG'] = json_decode($Config->getBody()->getContents());
}
}
This however results in a 500 internal server error from postman.
As it has been pointed out, the proper way to do this is via middleware :
First :
php artisan make:middleware BeforeRequest
Then in app\Http\Middleware\BeforeRequest.php :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Http\Controllers\Root\RootController;
use App\Http\Controllers\Root\ConfigRootController;
class BeforeRequest
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json'); //force the response to be json format
$data = $request->all();
RootController::setConn($data['url']);
$Config = ConfigRootController::get_tl_config($data['url']);
if(isset($data['cookie'])){
$User = ConfigRootController::getCurrentUser($data['url'],$data['cookie']);
$GLOBALS['user']= json_decode($User->getBody()->getContents());
}
$GLOBALS['TL_CONFIG'] = json_decode($Config->getBody()->getContents());
return $next($request);
}
}
Then in app\Http\Kernel.php :
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* #var array
*/
protected $middleware = [
\App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Fruitcake\Cors\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\BeforeRequest::class, //! add your middleware
];
And that is all.

Laravel API using modified 'MustVerifyApiEmail' and custom 'EnsureApiEmailIsVerified' middleware can't pick up request user

I have using Laravel 6.x as a backend with an external (different domain) Vue frontend and do not have register user functionality. The way I register users is by importing batches of users using the Maatwebsite/Laravel-Excel package - which works great.
So when each user is created a job is created by sending each user an email verification link, which when they login for the first time they will need to change their password and simultaneously their email gets marked as verified - which also should work fine.
The problem is that with the already created factory of users, who have their email_verified_at field filled - and the newly imported users - I cannot login as the custom EnsureEmailApiIsVerified middleware does not have access to the $request->user(). I figured out that I can specify the auth guard of 'api' such as $request->user('api') which then can pick up the user, but only of their Bearer token (using Laravel Passport) is sent with the request.
This does make sense as how else would the system know who the request user is without some identifier such as the token. But then how does the standard 'implements MustVerifyEmail' on the User model and subsequent standard 'EnsureEmailIsVerified' middelware on the web routes pick up the $request->user()?
It would stand to reason that either both (standard and my custom) middleware should have access to the $request->user() or both should not.
Now I have had to modify and bring out quite a few framework controllers into my App\Http directory but I have copied them almost verbatim just changing a few things to ensure it works with my API routes - because setting the default guard to 'api' instead of 'web' in config/auth.php had not used it as a default throughout my controllers as thought to be.
So here are the steps I followed:
Created a custom middleware and attached it to the entire 'api' middleware group in App\Http\Kernel.php
/**
* The application's route middleware groups.
*
* #var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class
],
'api' => [
'throttle:60,1',
'bindings',
'verifiedapi',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* #var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'role' => \App\Http\Middleware\HasRole::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'verifiedapi' => \App\Http\Middleware\EnsureApiEmailIsVerified::class,
];
Then here is that custom 'EnsureApiEmailIsVerified' middleware:
<?php
namespace App\Http\Middleware;
use Closure;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;
class EnsureApiEmailIsVerified
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (! $request->user('api') ||
($request->user('api') instanceof MustVerifyApiEmail &&
! $request->user('api')->hasVerifiedEmail())) {
return abort(403, 'Your email has not yet been verified.');
}
return $next($request);
}
}
You will see that references an instance of my custom 'MustVerifyApiEmail' which is a trait that is used on the User Model, with the only diversion from the standard trait being the public function 'sendApiEmailVerificationNotification' as such:
<?php
namespace App\Http\Controllers\API\Auth;
use App\Notifications\VerifyApiEmail;
trait MustVerifyApiEmail
{
/**
* Determine if the user has verified their email address.
*
* #return bool
*/
public function hasVerifiedEmail()
{
return ! is_null($this->email_verified_at);
}
/**
* Mark the given user's email as verified.
*
* #return bool
*/
public function markEmailAsVerified()
{
return $this->forceFill([
'email_verified_at' => $this->freshTimestamp(),
])->save();
}
/**
* Send the email verification notification.
*
* #return void
*/
public function sendApiEmailVerificationNotification()
{
$this->notify(new VerifyApiEmail);
}
/**
* Get the email address that should be used for verification.
*
* #return string
*/
public function getEmailForVerification()
{
return $this->email;
}
}
This new 'sendApiEmailVerificationNotification()' notifies the $request-user('api') with a custom VerifyApiEmail Notification, as such:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Config;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class VerifyApiEmail implements ShouldQueue
{
use Queueable;
/**
* The callback that should be used to build the mail message.
*
* #var \Closure|null
*/
public static $toMailCallback;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$verificationUrl = $this->verificationUrl($notifiable);
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
}
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'), $verificationUrl)
->line(Lang::get('If you did not create an account, no further action is required.'));
}
/**
* Get the verification URL for the given notifiable.
*
* #param mixed $notifiable
* #return string
*/
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.api.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
/**
* Set a callback that should be used when building the notification mail message.
*
* #param \Closure $callback
* #return void
*/
public static function toMailUsing($callback)
{
static::$toMailCallback = $callback;
}
}
The new api routes are as follows:
Route::namespace('API\Auth')->group(function () {
Route::post('login', 'PassportController#login');
Route::post('refresh', 'PassportController#refresh');
Route::post('logout', 'PassportController#logout');
Route::get('email/verify/{id}/{hash}', 'VerificationApiController#verify')->name('verification.api.verify');
Route::get('email/resend', 'VerificationApiController#resend')->name('api.verification.resend');
});
And the VerificationApiController is as follows:
<?php
namespace App\Http\Controllers\API\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Auth\Access\AuthorizationException;
class VerificationApiController extends Controller
{
/**
* Show the email verification notice.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function show(Request $request)
{
//
}
/**
* Mark the authenticated user's email address as verified.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*
* #throws \Illuminate\Auth\Access\AuthorizationException
*/
public function verify(Request $request)
{
if (! hash_equals((string) $request->route('id'), (string) $request->user('api')->getKey())) {
throw new AuthorizationException;
}
if (! hash_equals((string) $request->route('hash'), sha1($request->user('api')->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($request->user('api')->hasVerifiedEmail()) {
return response()->json(['error' => 'Email already verified'], 422);
}
if ($request->user('api')->markEmailAsVerified()) {
event(new Verified($request->user('api')));
}
return response()->json(['success' => 'Email verified!']);
}
/**
* Resend the email verification notification.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function resend(Request $request)
{
$request->user('api')->sendApiEmailVerificationNotification();
return response()->json(['success' => 'Email verification has been resent!']);
}
}
I also noticed that onn the User Model it extends User as Authenticatable which then uses the standard MustVerifyEmail' - so I brought that out of the framework as well and changed the usage to the new MustVerifyApiEmail - like so:
<?php
namespace App\Http\Controllers\API\Auth;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword, MustVerifyApiEmail;
}
My User model then looks like this at the top:
<?php
namespace App;
use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use App\Http\Controllers\API\Auth\User as Authenticatable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmailInterface;
class User extends Authenticatable implements MustVerifyApiEmailInterface
{
use HasApiTokens, Notifiable;
...
As you can see it's quite a bit of customization - but it should all work in theory and I am getting no errors that I can use. Here are the errors that I get:
When I login with a user who's email is verified or even not verified, I get the error that the user's email has not been verified - but only because it doesn't pick up the $request->user('api'). When I try to throw an error in the middleware itself before returning the request dumping the $request->user('api') it gives me null
So my question is, with the standard middleware of 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - how does this pick up the $request->user()?
Is there something I am missing or am I going about this the wrong way? It seems that when the user logs in it doesn't log them in and then run the middleware - so there is no $request->user('api') - maybe because I am using Passport, but I would think that what should happen is that the middleware needs to run after it has authenticated the user then it would have access to the $request->user('api')
ANY GUIDANCE WOULD BE EXTREMELY APPRECIATED!
api.php
Route::prefix('email')->middleware(['auth:api'])->group(function () {
Route::get('resend', 'VerificationController#resend')->name('verification.resend');
Route::get('verify/{id}/{hash}', 'VerificationController#verify')->name('verification.verify');
});

Laravel API using modified MustVerifyEmail trait cannot pick up request user

I have using Laravel 6.x as a backend with an external (different domain) Vue frontend and do not have register user functionality. The way I register users is by importing batches of users using the Maatwebsite/Laravel-Excel package - which works great.
So when each user is created a job is created by sending each user an email verification link, which when they login for the first time they will need to change their password and simultaneously their email gets marked as verified - which also should work fine.
The problem is that with the already created factory of users, who have their email_verified_at field filled - and the newly imported users - I cannot login as the custom EnsureEmailApiIsVerified middleware does not have access to the $request->user(). I figured out that I can specify the auth guard of 'api' such as $request->user('api') which then can pick up the user, but only of their Bearer token (using Laravel Passport) is sent with the request.
This does make sense as how else would the system know who the request user is without some identifier such as the token. But then how does the standard 'implements MustVerifyEmail' on the User model and subsequent standard 'EnsureEmailIsVerified' middelware on the web routes pick up the $request->user()?
It would stand to reason that either both (standard and my custom) middleware should have access to the $request->user() or both should not.
Now I have had to modify and bring out quite a few framework controllers into my App\Http directory but I have copied them almost verbatim just changing a few things to ensure it works with my API routes - because setting the default guard to 'api' instead of 'web' in config/auth.php had not used it as a default throughout my controllers as thought to be.
So here are the steps I followed:
Created a custom middleware and attached it to the entire 'api' middleware group in App\Http\Kernel.php
/**
* The application's route middleware groups.
*
* #var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
// \Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class
],
'api' => [
'throttle:60,1',
'bindings',
'verifiedapi',
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* #var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'role' => \App\Http\Middleware\HasRole::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'verifiedapi' => \App\Http\Middleware\EnsureApiEmailIsVerified::class,
];
Then here is that custom 'EnsureApiEmailIsVerified' middleware:
<?php
namespace App\Http\Middleware;
use Closure;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;
class EnsureApiEmailIsVerified
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if (! $request->user('api') ||
($request->user('api') instanceof MustVerifyApiEmail &&
! $request->user('api')->hasVerifiedEmail())) {
return abort(403, 'Your email has not yet been verified.');
}
return $next($request);
}
}
You will see that references an instance of my custom 'MustVerifyApiEmail' which is a trait that is used on the User Model, with the only diversion from the standard trait being the public function 'sendApiEmailVerificationNotification' as such:
<?php
namespace App\Http\Controllers\API\Auth;
use App\Notifications\VerifyApiEmail;
trait MustVerifyApiEmail
{
/**
* Determine if the user has verified their email address.
*
* #return bool
*/
public function hasVerifiedEmail()
{
return ! is_null($this->email_verified_at);
}
/**
* Mark the given user's email as verified.
*
* #return bool
*/
public function markEmailAsVerified()
{
return $this->forceFill([
'email_verified_at' => $this->freshTimestamp(),
])->save();
}
/**
* Send the email verification notification.
*
* #return void
*/
public function sendApiEmailVerificationNotification()
{
$this->notify(new VerifyApiEmail);
}
/**
* Get the email address that should be used for verification.
*
* #return string
*/
public function getEmailForVerification()
{
return $this->email;
}
}
This new 'sendApiEmailVerificationNotification()' notifies the $request-user('api') with a custom VerifyApiEmail Notification, as such:
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\Facades\Lang;
use Illuminate\Support\Facades\Config;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
class VerifyApiEmail implements ShouldQueue
{
use Queueable;
/**
* The callback that should be used to build the mail message.
*
* #var \Closure|null
*/
public static $toMailCallback;
/**
* Create a new notification instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Get the notification's delivery channels.
*
* #param mixed $notifiable
* #return array
*/
public function via($notifiable)
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* #param mixed $notifiable
* #return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
$verificationUrl = $this->verificationUrl($notifiable);
if (static::$toMailCallback) {
return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
}
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'), $verificationUrl)
->line(Lang::get('If you did not create an account, no further action is required.'));
}
/**
* Get the verification URL for the given notifiable.
*
* #param mixed $notifiable
* #return string
*/
protected function verificationUrl($notifiable)
{
return URL::temporarySignedRoute(
'verification.api.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
[
'id' => $notifiable->getKey(),
'hash' => sha1($notifiable->getEmailForVerification()),
]
);
}
/**
* Set a callback that should be used when building the notification mail message.
*
* #param \Closure $callback
* #return void
*/
public static function toMailUsing($callback)
{
static::$toMailCallback = $callback;
}
}
The new api routes are as follows:
Route::namespace('API\Auth')->group(function () {
Route::post('login', 'PassportController#login');
Route::post('refresh', 'PassportController#refresh');
Route::post('logout', 'PassportController#logout');
Route::get('email/verify/{id}/{hash}', 'VerificationApiController#verify')->name('verification.api.verify');
Route::get('email/resend', 'VerificationApiController#resend')->name('api.verification.resend');
});
And the VerificationApiController is as follows:
<?php
namespace App\Http\Controllers\API\Auth;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Auth\Events\Verified;
use Illuminate\Auth\Access\AuthorizationException;
class VerificationApiController extends Controller
{
/**
* Show the email verification notice.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function show(Request $request)
{
//
}
/**
* Mark the authenticated user's email address as verified.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*
* #throws \Illuminate\Auth\Access\AuthorizationException
*/
public function verify(Request $request)
{
if (! hash_equals((string) $request->route('id'), (string) $request->user('api')->getKey())) {
throw new AuthorizationException;
}
if (! hash_equals((string) $request->route('hash'), sha1($request->user('api')->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($request->user('api')->hasVerifiedEmail()) {
return response()->json(['error' => 'Email already verified'], 422);
}
if ($request->user('api')->markEmailAsVerified()) {
event(new Verified($request->user('api')));
}
return response()->json(['success' => 'Email verified!']);
}
/**
* Resend the email verification notification.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function resend(Request $request)
{
$request->user('api')->sendApiEmailVerificationNotification();
return response()->json(['success' => 'Email verification has been resent!']);
}
}
I also noticed that onn the User Model it extends User as Authenticatable which then uses the standard MustVerifyEmail' - so I brought that out of the framework as well and changed the usage to the new MustVerifyApiEmail - like so:
<?php
namespace App\Http\Controllers\API\Auth;
use Illuminate\Auth\Authenticatable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\Access\Authorizable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmail;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
class User extends Model implements
AuthenticatableContract,
AuthorizableContract,
CanResetPasswordContract
{
use Authenticatable, Authorizable, CanResetPassword, MustVerifyApiEmail;
}
My User model then looks like this at the top:
<?php
namespace App;
use Illuminate\Support\Str;
use Laravel\Passport\HasApiTokens;
use Illuminate\Notifications\Notifiable;
use App\Http\Controllers\API\Auth\User as Authenticatable;
use App\Http\Controllers\API\Auth\MustVerifyApiEmailInterface;
class User extends Authenticatable implements MustVerifyApiEmailInterface
{
use HasApiTokens, Notifiable;
...
As you can see it's quite a bit of customization - but it should all work in theory and I am getting no errors that I can use. Here are the errors that I get:
When I login with a user who's email is verified or even not verified, I get the error that the user's email has not been verified - but only because it doesn't pick up the $request->user('api'). When I try to throw an error in the middleware itself before returning the request dumping the $request->user('api') it gives me null
So my question is, with the standard middleware of 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, - how does this pick up the $request->user()?
Is there something I am missing or am I going about this the wrong way? It seems that when the user logs in it doesn't log them in and then run the middleware - so there is no $request->user('api') - maybe because I am using Passport, but I would think that what should happen is that the middleware needs to run after it has authenticated the user then it would have access to the $request->user('api')
ANY GUIDANCE WOULD BE EXTREMELY APPRECIATED!
Hopefully you have long since solved this one, but since I came across this same problem earlier I thought I'd share my solution. I wanted to use verify on login of the api, but realised that this would be invoked before the user was logged in or authenticated which means there is no user on the request.
In my case I made changes to my custom EnsureEmailsVerified like this:
<?php
namespace App\Http\Middleware;
use App\User;
use Closure;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class EnsureApiEmailIsVerified
{
/**
* Handle an incoming request.
*
* #param Request $request
* #param \Closure $next
* #return JsonResponse
*/
public function handle($request, Closure $next)
{
$user = $request->user() ?? User::where('email', $request->get('username'))->first();
if (! $user ||
($user instanceof MustVerifyEmail &&
! $user->hasVerifiedEmail())) {
return response()->json(['error' => [
'message' => __('errors.email_not_verified'),
'status_code' => 401,
]], 401);
}
return $next($request);
}
}
And likewise I made a custom VerifiesEmails trait and changed the verify method to find the user based on the id in the email link because I know this will be going to new, non-logged in users:
public function verify(Request $request)
{
$user = User::find($request->get('id'));
if (! hash_equals((string) $request->get('id'), (string) $user->getKey())) {
throw new AuthorizationException;
}
if (! hash_equals((string) $request->get('hash'), sha1($user->getEmailForVerification()))) {
throw new AuthorizationException;
}
if ($user->hasVerifiedEmail()) {
return new Response('', 204);
}
if ($user->markEmailAsVerified()) {
event(new Verified($user));
}
if ($response = $this->verified($request)) {
return $response;
}
return new Response('', 204);
}

Form Request Validation not working. It just loads the home page in postman

I am trying to validate input data with a custom form request.
The form request name is UpdatePassword. And i try to run the code by injecting it in my updatePassword() function in my Api/AuthController. I and it just redirects back to home page
<?php
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
class UpdatePassword extends FormRequest
{
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|string|email|max:255|exists:users,email',
'old_password' => 'required|string',
'password' => 'required|string|confirmed'
];
}
}
My controller
<?php
namespace App\Http\Controllers\API;
use App\Http\Requests\Api\UpdatePassword;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class AuthController extends controller
{
public function updatePassword(UpdatePassword $request){
dd($request->all());
}
}
My Exceptions/Handler.php
<?php
namespace App\Exceptions;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* #var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* #var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* #param \Exception $exception
* #return void
*/
public function report(Exception $exception)
{
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* #param \Illuminate\Http\Request $request
* #param \Exception $exception
* #return \Illuminate\Http\Response
*/
public function render($request, Exception $exception)
{
return parent::render($request, $exception);
}
}
Here is my route
<?php
use Illuminate\Http\Request;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| is assigned the "api" middleware group. Enjoy building your API!
|
*/
Route::middleware('auth:api')->get('/user', function (Request $request) {
return $request->user();
});
Route::group(['namespace' => 'Api'], function (){
Route::group(['prefix' => 'user'], function (){
Route::post('changePassword', 'AuthController#updatePassword');
});
});
Here is the postman request and response
[![Here is the postman request and response][1]][1]
when I
try to run the code in postman it as an ajax request it doesn't work. the validation doesn't work. it just renders the home page. localhost:8000
Are you calling the route from postman with the Accept: application/json HTTP header (or the equivalent Content-Type one)?
If you don't put that Laravel doesn't know that you want a json response and would redirect you to the homepage (I think it will default to it because you don't have a referrer header set on your request) with errors in the session.
If validation fails while using a Form request, a redirect response will be generated to send the user back to their previous location. That's why you are redirecting back to the page instead of the response JSON.
Laravel have one protected method "failedValidation" method. You need to overwrite this method in your form request class which is UpdatePassword.php in case above by adding the below code snippet to.
.
.
.
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json($validator->errors(), 422));
}
you can try with this validator syntex. i have used.
$validator = Validator::make($request->all(), [
'email' => 'required|email',
'password' => 'required',
]);
$temp = $validator->errors()->all();
if ($validator->fails()) {
return response()->json(['Status' =>
false,'Message'=>$temp[0],'Data' => '','Status_code' =>"401" ]);
}

Categories