Laravel 5.2 SAML2 Authentication persistance issue - php

I have installed this github Laravel 5 Saml2 package:
https://github.com/aacotroneo/laravel-saml2
I've got the login working, authentication is happening and the data is being passed back correctly. I have a LoginListener that is successfully catching the correct user information and returning a valid user from the database. However when I attempt to use Auth::login it does not persist outside of the listener handle function and will go into an endless loop between the SAML authentication and the listener.
Here is my Listener:
namespace App\Listeners;
use \Aacotroneo\Saml2\Events\Saml2LoginEvent;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Auth;
use Illuminate\Session\Middleware\StartSession;
use App\Http\Controllers\HomeController;
class LoginListener
{
/**
* Create the event listener.
*
* #return void
*/
public function __construct()
{
}
/**
* Handle the event.
*
* #param Saml2LoginEvent $event
* #return void
*/
public function handle(Saml2LoginEvent $event)
{
$user = $event->getSaml2User();
$userData = [
'id' => $user->getUserId(),
'attributes' => $user->getAttributes(),
'assertion' => $user->getRawSamlAssertion()
];
//check if email already exists and fetch user
$user = \App\Models\User::where('username', $userData['attributes']['NameID'][0])->first();
Auth::guard('web')->login($user);
Session::save();
}
}
I have added 'web' to my routesMiddleware setting in the saml2-settings file as is suggested in multiple places with no effect. Does someone have a working example of this somewhere that I can dig through to see what I am doing wrong?

The solution to this turned out to be in the middleware. I have several custom middleware files setup and they were interferring with the web middleware begin the solution to the issue.
To solve this I created a custom middlewareGroup in the Kernel called listener and removed all of the extra middleware that I had added to web:
'listener' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
],
Then in the saml2_settings config file I changed the routesMiddleware variable to listener:
'routesMiddleware' => ['listener'],
Hopefully this helps someone else in the troubleshooting process.

Related

Laravel test for URL param in middleware BEFORE group Middleware

I am using a middleware to check for the existence of a login token from another site. If the login token is present and the user is not already logged in, I would like to use the token to log the user in and send them to their intended page. If they are already logged in, I would like it to do nothing.
As suggested, should this be a ServiceProvider instead?
Here is my middleware:
<?php
namespace App\Http\Middleware;
use Session;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Auth\LoginController;
class CheckRepLoginToken
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle(Request $request, Closure $next){
$loginToken = $request->repLoginToken;
$goto = '/'.$request->path();
if(isset($loginToken) && Auth::guest()){
(new LoginController)->login($loginToken,$goto);
}
return $next($request);
}
}
The problem is that I need this to all run prior to the $middlewareGroups and $routeMiddleware so the user IS NOT sent to the login screen if Auth::guest() is true but the token is present.
I currently have the middleware in the protected $middleware section of the Kernel and everyone seems to be a "guest" whether or not they are logged in.
This is the kernel file:
<?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\CheckRepLoginToken::class,
// 'checkStatus' => \App\Http\Middleware\CheckStatus::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' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* 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,
'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,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
How can I achieve my desired result without messing with the current authentication?
At first, middleware seems like the right tool for a one-time login token feature, but the implementation can be tricky without an understanding of Laravel's request pipeline. At the end of this answer, we'll look at a simple alternative that uses custom authentication functionality instead.
For traditional web requests from a browser, Laravel's authentication services depend on the existence of a session identified by a cookie. The question's CheckRepLoginToken middleware is declared in the global middleware array in Kernel.php, and these handlers execute before route middleware which include the StartSession middleware in the 'web' group.
Since the StartSession middleware initializes session state for a request, authentication context for a global CheckRepLoginToken middleware is not available yet. Calling Auth::guest() from the global middleware will always return true for the configuration shown in the question. I'm not sure what the LoginController::login() method does in your particular project, but I imagine that the auth state set-up by attempting to invoke that method from global middleware may disappear when the standard session and auth middleware run afterward.
Depending on what your LoginController::login() method does, it may be enough to move the declaration for the CheckRepLoginToken middleware below StartSession in the 'web' group. As an aside, some might consider it bad practice to instantiate the controller to call the method directly. We can achieve a similar outcome without much code:
public function handle(Request $request, Closure $next)
{
if ($request->has('repLoginToken') && Auth::guest()) {
$user = // ...try to fetch a user with $request->repLoginToken...
if ($user !== null) {
Auth::login($user);
}
}
return $next($request);
}
A more complete solution takes advantage of Laravel's pluggable authentication system. We can wrap Laravel's standard authentication guard with a custom implementation that handles the token.
First, we'll update config/auth.php to switch the default 'web' guard to use a custom driver that we'll implement below. We rename the original 'web' guard to 'session' so that we can refer to it later.
'guards' => [
'web' => [
'driver' => 'rep-token',
],
'session' => [
'driver' => 'session',
'provider' => 'users',
]
],
Laravel's AuthManager includes a helper method—viaRequest()—that simplifies the creation of a Guard that authenticates a user with data from the request context without the need to fully-implement Illuminate\Contracts\Auth\Guard. We bind our custom guard in the boot() method in AuthServiceProvider.php:
public function boot()
{
Auth::viaRequest('rep-token', function ($request) {
$baseGuard = Auth::guard('session');
if ($request->has('repLoginToken') && $baseGuard->guest()) {
$user = // ...try to fetch a user with $request->repLoginToken...
if ($user !== null) {
$baseGuard->login($user);
}
}
return $baseGuard->user();
});
}
As we can see, this wrapper reuses the functionality of Laravel's standard session-based authentication and handles the special case for the presence of repLoginToken. It does not need any additional middleware.
Since this post is a public, I feel obligated to emphasize a point from Mtxz's answer. Exercise great caution in the design and implementation of a third-party authentication scheme. In general, anyone who obtains a valid token—including the third-party—has complete access to a user's account. The simplicity of the authentication flow described in this question suggests vulnerabilities that may not be acceptable for many applications.
try to name your route and do like this here:
Route::get('/login/{loginToken}', 'LoginController#login')->name('login.route');
if (isset($loginToken) && Auth::guest()) {
return redirect()->route('login.route', [
'token' => $loginToken
])
}
Actually, for Laravel to know if the user is logged in or not, the request needs to passe the auth middleware. So your custom middleware would need to be triggered after the auth one.
So if your middleware need to know if the user is logged in or not, the auth middleware have to be passed first (also meaning it'll only work for routes or route groups under the auth middleware - as I see the auth middleware is not in your default app middleware stack).
And if the auth middleware redirects the user, then yours is never called.
override the Auth middleware
as your business is "auth related", you could easily override the authenticate middleware you are using. Copy or extend the vendor class, replace the auth middleware class with yours in the Kernel, and add your custom business after the default middleware business identified or not the user as logged in.
I guess you could also go by creating a custom authentication Guard and use it instead of the default one, but I think the custom auth middleware is faster.
I don't know much about your custom auth business, but be careful about tokens in URL (that can be sent by mail, or saved elsewhere) that authenticate a user: those tokens should expire after a delay and after use. Also, you should prevent brute-forcing of this parameter.

Target class [App\Listeners\CartUpdatedListener] does not exist

i want to configure events in choping cart uses LaravelShoppingcart package, i run cmd following php artisan make: listener CartUpdateListener, but it gives me error Target class [App \ Listeners \ CartUpdatedListener] does not exist.
EventServiceProvider.php
protected $listen = [
'cart.added' => [
'App\Listeners\CartUpdatedListener',
],
'cart.updated' => [
'App\Listeners\CartUpdatedListener',
],
'cart.removed' => [
'App\Listeners\CartUpdatedListener',
],
];
CartUpdateListener.php
/**
* Handle the event.
*
* #param object $event
* #return void
*/
public function handle($event)
{
dd("event was fired");
}
Your error tells us about CardUpdatedListener but your file name is CardUpdateListener (an extra 'd' in the word Update). Check it, or post full class code.
When you run an Artisan command, it bootstraps a Laravel app. Among a lot of things, event listeners are setup and they need to be constructed, but at this moment, your listener class does not exist yet. Follow these steps:
Remove everything from your listen property of EventServiceProvider
Run php artisan make:listener CartUpdatedListener
Refill your listen property

Undefined index: path, when adding new custom service provider in laravel 5.8

I want to add new custom service provider to set session_lifetime. The value for it, I got it from database. For do this thing I create service provider. But after the service provider registered. I got and error like this : Undefined index: path.
This is my new service provider code :
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Config;
use Illuminate\Support\Facades\DB;
class ParameterSettingServiceProvider extends ServiceProvider
{
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
}
/**
* Register services.
*
* #return void
*/
public function register()
{
if (\Schema::hasTable('parameter_settings')) {
$settings = DB::table('parameter_settings')->first();
if ($settings) //checking if table is not empty
{
$config = array(
'driver' => env('SESSION_DRIVER', 'file'),
'lifetime' => env('SESSION_LIFETIME', $settings->session_expired),
'expire_on_close' => true,
);
Config::set('session', $config);
}
}
}
}
And this is my config/app.php to register the service provider :
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\ParameterSettingServiceProvider::class,
How to fix this error?
You should be using the boot method of the Service Provider not the register method. The boot method of the Provider will run after all other Service Provider's register methods have ran.
If you only want to set 1 single configuration value, you can do that. You don't need to overwrite the entire session config key (which holds 15 vars, check your config/session.php file). This is why you will keep getting errors about undefined indexes because these configuration variables need to exist.
The documentation's example for setting a config value at run time is how you set a single key by name (using the "dot" syntax):
Config::set('session.lifetime', $settings->session_expired);
Laravel 5.8 Docs - Configuration - Accessing Configuration Values

FormRequest not populating $request->old() after validation failure

My request object isn't receiving old() data when the form fails validation. I am receiving the error messages, but none of the old input data.
I've read a few solutions on similar questions that reference making changes to the redirect on the controller, but that won't solve this problem because the redirect is done by the function referenced below within the FormRequest Class and not the Controller.
Has anyone else experienced this same issue? I've upgraded my instance as I read a few forums that referenced existing bugs, but the issue still exists.
Any help would be appreciated.
Versions: I've tried this on laravel 5.4, 5.7 and 5.8 and none of them are rendering old data.
How I'm doing Request Validation
The validation is being done via the standard Requests file that extends FormRequest.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CustomerManagementRequest 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 [
'first_name' => 'required|min:2',
'last_name' => 'required|min:2',
'email' => 'required|min:4',
];
}
}
How I'm trying to access the old data on my view:
value="{{old('first_name')}}"
The Redirect and validation being done in FormRequest
The redirect is being done via the Laravel standard FormRequest class.
protected function failedValidation(Validator $validator)
{
throw (new ValidationException($validator))
->errorBag($this->errorBag)
->redirectTo($this->getRedirectUrl());
}
The Validator Response: This is what I see when I vardump the above function. It does have my form data.
Validator {#629 ▼
#data: array:11 [▼
"_token" => "1ynKXxi551UBGbJq6ftLsW6VsClZzmbZdHiHIxyt"
"active_from_date" => "04/04/2019 10:58 PM"
"last_sync_date" => "04/04/2019 11:00 PM"
"first_name" => "Pizza"
"last_name" => "Dough"
"email" => null
"full_phone" => null
"phone" => null
]
}
vardumping the old() parameter in the view returns an empty array
[]
After validation you should check all validation is passed or not. In controller's method you can do like this,
$validator = Validator::make($request->all(), $rules);
if ($validator->fails()) {
$messages = $validator->errors()->getMessages();
return back()
->withErrors($messages)
->withInput();
}
Hope this helps :)
I found the solution to this and thought I'd share it in case it helps anyone out there wracking their brains trying to figure a similar issue out.
I had a custom middleware called ViewData that had a session()->save() command at the end of it which was saving over flashed data, thus causing my array of old inputs to be empty.
Here is how I stepped through it for those of you debugging a similar issue.
First I tried to create a manual validation in my controller to overrule an issue with FormRequest. I then followed the validator to the ShareErrorsFromSession middleware in the kernel.
I realized I had a middleware that ran after this middleware (I call it ViewData) and that middleware stores some variables to session.
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\ViewData::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
I scrolled through and found a session()->save(). This save saved over the work being done by the withInput, essentially keeping old empty. I removed the save session (as it wasn't of value) and now old() contains the data expected.

Lumen - Mono Log - Create Custom log file [duplicate]

I want to add to my Lumen project a daily Log.
I try this in the app.php (Folder Bootstrap/)
$logFile = 'laravel.log';
Log::useDailyFiles(storage_path().'/logs/'.$logFile);
But this set me that error
Call to undefined method Monolog\logger::useDailyFiles()
Any help I appreciate...Thanks
If you look at the framework source code here you can see that it will not do daily logs, but rather write to a single log file lumen.log. There is a public method available configureMonologUsing seen here and referenced here that you can use to override the default behavior without extending the Application.
Lumen just sets a handler to monolog, so another good solution is you could do this:
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\RotatingFileHandler;
class LogServiceProvider extends ServiceProvider
{
/**
* Configure logging on boot.
*
* #return void
*/
public function boot()
{
$maxFiles = 5;
$handlers[] = (new RotatingFileHandler(storage_path("logs/lumen.log"), $maxFiles))
->setFormatter(new LineFormatter(null, null, true, true));
$this->app['log']->setHandlers($handlers);
}
/**
* Register the log service.
*
* #return void
*/
public function register()
{
// Log binding already registered in vendor/laravel/lumen-framework/src/Application.php.
}
}
Then don't forget to add the service provider to your Lumen bootstrap/app.php:
$app->register(\App\Providers\LogServiceProvider::class);
In Lumen 5.6 better way is to configure your default setting in .env as LOG_CHANNEL=daily
By default the setting is LOG_CHANNEL=stack which use single file for logging.
Starting from version 5.6, configuring the logging system is much easier:
Create directory config in your project if it doesn't exist
Copy the logging config file from
vendor/laravel/lumen-framework/config/logging.php to your project config dir
Edit file config/logging.php and adjust the channels property to your liking.
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['daily'],
],

Categories