I am building APIs for my Android app using laravel and default session driver set to REDIS.
I found a good article here http://dor.ky/laravel-prevent-sessions-for-routes-via-a-filter/ which sort of serves the purpose.
However when ever I hit the url it also hits the redis and generates the key which is empty. Now I want avoid creating empty session keys in redis. Ideally it should not hit the redis How can I do that?
Can we customise sessios in a way so that sessions are generated only for specific routes (or disable for specific routes)?
I can explain more with specific use case, please let me know.
Its really easy using the middleware in Laravel 5, I needed any request with an API key not to have a session and I simply did :
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Session\Middleware\StartSession as BaseStartSession;
class StartSession extends BaseStartSession
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next)
{
if(\Request::has('api_key'))
{
\Config::set('session.driver', 'array');
}
return parent::handle($request, $next);
}
}
Also you will need to extend the SessionServiceProvider as follows:
<?php namespace App\Providers;
use Illuminate\Session\SessionServiceProvider as BaseSessionServiceProvider;
class SessionServiceProvider extends BaseSessionServiceProvider
{
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerSessionManager();
$this->registerSessionDriver();
$this->app->singleton('App\Http\Middleware\StartSession');
}
}
and place in your config/app.php under providers:
'App\Providers\SessionServiceProvider',
Also you must change it in your kernel file: App/Http/Kernel.php, in the $middlewareGroups section change the default entry, \Illuminate\Session\Middleware\StartSession::class, to your new class \App\Http\Middleware\StartSession::class,.
In Laravel 5, just don't use the StartSession, ShareErrorsFromSession, and VerifyCsrfToken middlewares.
In my application I've moved these three middlewares from the web group to a new stateful group, and then I have included this stateful group on routes which need to know about the session (in addition to web in all cases, in my app at least). The other routes belong to either the web or api groups.
Now when making requests to the routes which are not using the stateful middleware group session cookies are not sent back.
The simplest way to achieve this is to Make your own AppStartSession middleware that subclasses Illuminate\Session\Middleware\StartSession and the replace the class being used in kernel.php. The only method you need to override in your subclass is sessionConfigured() for which you can return false to disable the session or parent::sessionConfigured() to allow it.
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Session\Middleware\StartSession;
class AppStartSession extends StartSession
{
protected function sessionConfigured(){
if(!\Request::has('api_key')){
return false;
}else{
return parent::sessionConfigured();
}
}
}
kernel.php (see *** comment for where the change is done)
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* #var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
// *** Replace start session class
// \Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\AppStartSession::class,
// *** Also comment these ones that depend on there always being a session.
//\Illuminate\View\Middleware\ShareErrorsFromSession::class,
//\App\Http\Middleware\VerifyCsrfToken::class,
];
/**
* The application's route middleware.
*
* #var array
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
];
}
Don't fight the framework, embrace it!
Since Laravel 5.2, when middleware groups were introduced, you may disable session for certain routes by defining them outside of the "web" middleware group (which includes the StartSession middleware responsible for session handling). As on latest 5.2.x versions the whole default routes.php file is wrapped with "web" middleware group, you need to make some modification in app/Providers/RouteServiceProvider.php file, as described here.
There appears to be a way to accomplish this using a session reject callback.
Relevant sources...
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Foundation/Application.php#L655
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Foundation/Application.php#L660
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Session/Middleware.php#L60
https://github.com/laravel/framework/blob/4.2/src/Illuminate/Session/Middleware.php#L97
I can't find many references to this around the web, but reading more through the source it appears that if the session reject callback returns a truthy value, the session will be forced to use an array driver for the request rather than whatever is configured. Your callback also gets the current request injected so you can do some logic based on the request parameters.
I've only tested this on a local Laravel 4.2 install but it seems to work. You just need to bind a function to session.reject.
First, create a SessionRejectServiceProvider (or something like that)
<?php
use \Illuminate\Support\ServiceProvider;
class SessionRejectServiceProvider extends ServiceProvider {
public function register()
{
$me = $this;
$this->app->bind('session.reject', function($app)use($me){
return function($request)use($me){
return call_user_func_array(array($me, 'reject'), array($request));
};
});
}
// Put the guts of whatever you want to do in here, in this case I've
// disabled sessions for every request that is an Ajax request, you
// could do something else like check the path against a list and
// selectively return true if there's a match.
protected function reject($request)
{
return $request->ajax();
}
}
Then add it to your providers in your app/config/app.php
<?php
return array(
// ... other stuff
'providers' => array(
// ... existing stuff...
'SessionRejectServiceProvider',
),
);
Edit / More Info
The net result is that the reject() method is called on every request to your application, before the session is started. If your reject() method returns true, sessions will be set to the array driver and basically do nothing. You can find a lot of useful info the $request parameter to determine this, here's the API reference for the request object in 4.2.
http://laravel.com/api/4.2/Illuminate/Http/Request.html
I've been trying to accomplish a similar feature.
Our API is stateless except for 1 route - the version 1 cart.
I ended up with setting 'driver' in the app/config/session.php like this ...
'driver' => 'v1/cart' === Request::getDecodedPath() ? 'native' : 'array',
Nothing magic. Initially we though of using a before filter, but that wasn't happening early enough.
It seems a simple way to do things, but I may be missing something.
Putting the switch in the config seems an easy place for other developers to see what the driver is whereas putting it in a service provider is so tucked out of the way, without knowing what service providers are installed and what they interact with, it would be far harder to debug.
Anyway. Hope this is of some use.
As pointed out below ... DO NOT CACHE YOUR CONFIG IF IT IS DYNAMIC.
Which does lead to it being of limited use. As soon as we no longer need to support v1/cart, we will be dropping this route and then be back on a static config.
Laravel default have two routes group called web and api, the api routes group default without session.
So, we can write any route role to routes/api.php, will not use session default.
If not want to use the api prefix url, we can modify app\Providers\RouteServiceProvider add a new group like this:
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/static.php'));
Now you can place any routes into routes/static.php file will not to use session.
Hope helpful.
Laravel 5x
In the App\Providers\RouteServiceProvider file, just copy the mapApiRoutes() method to a new method called mapStaticRoutes(), remove the prefix('api') call, and add "routes/static.php" (you will need to create this file). This will use the same stateless "api" middleware and not have an /api prefix assigned to the routes.
protected function mapStaticRoutes()
{
Route::middleware('api')
->namespace($this->namespace)
->group(base_path('routes/static.php'));
}
Just update the "map()" method to call "$this->mapStaticRoutes();" so that it knows of your new file. And any route added there should now be stateless and it wasn't much work.....
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
// Static Routes (stateless, no /api prefix)
$this->mapStaticRoutes();
}
static.php
// Health Check / Status Route (No Auth)
Route::get('/status', function() {
return response()->json([
'app' => 'My Awesome App',
'status' => 'OK'
]);
});
Related
I am using subdomain routing heavily within my project, as it's a multi-tenant application with each tenant having their own subdomain.
As a result, all of my routes are wrapped in:
Route::domain('{tenant}.'.config('app.base_url'))->group(function () {
// My routes here!
});
To use the route() helper within my code, I need to pass it all of the route parameters associated with it. Every single route has tenant associated with it, so I constantly found myself repeating code and writing route('my-route-name', ['tenant' => $request->route('tenant')]);
I've created a middleware with the following code in it:
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use URL;
class SetTenantFromRequest
{
public function handle(Request $request, Closure $next)
{
list($subdomain) = explode('.', $request->getHost(), 2);
URL::defaults(['tenant' => $subdomain]);
return $next($request);
}
}
And placed it in the HTTP Kernel.php file like so:
protected $middleware = [
TrustProxies::class,
CheckForMaintenanceMode::class,
ValidatePostSize::class,
TrimStrings::class,
ConvertEmptyStringsToNull::class,
WebHeaders::class,
SetLanguage::class,
SetTenantFromRequest::class,
];
// Removed for brevity...
protected $middlewarePriority = [
StartSession::class,
ShareErrorsFromSession::class,
SetTenantFromRequest::class,
Authenticate::class,
ThrottleRequests::class,
AuthenticateSession::class,
SubstituteBindings::class,
Authorize::class,
];
I had to add it to the $middlewarePriority array as it needs to run before the Authenticate middleware. Since Authenticate calls return route('login');, I need that URL parameter available beforehand.
Now with doing all of this, I still get the following error thrown from the Authenticate middleware: Missing required parameters for [Route: login] [URI: login].
If I run ddd(URL::getDefaultParameters()); within the Authenticate middleware, it prints an empty array. However if I run the same ddd(...) within the SetTenantFromRequest middleware, it shows the tenant in there as I expect it to be.
Does anyone have an idea on how I can solve this problem?
Moving this from the global middleware stack to the web middleware stack alleviates the issue.
This forces me to add the tenant parameter to the route(...) call within my Authenticate middleware, but it does allow me to forgo it everywhere else.
I have a multi-tenant app I'm working on and while adding the socialite package, I tried to load the custom facebook client_id and client_secret for the specific website from the database. I can't really use the env variables because each site will have it's own custom facebook keys.
It seems you can't really call a model's method on the config/services.php file because it might not have been loaded yet. I've tried going through the request lifecycle docs to resolve this to no avail.
I've also tried to create a service provider to get the value from my Business model's method and set it as a constant but still, by the time it's available in the app, the config/services.php file has been loaded.
Here's where I want the database value available:
config/services.php
'facebook' => [
'client_id' => \App\Business::getAppKeys()->fb_client_id,
'client_secret' => 'your‐fb‐app‐secret',
'redirect' => 'http://your‐callback‐url',
],
Error:
Fatal error: Call to a member function connection() on null
Here's a really quick example of what I'm talking about.
Using the following, I am able to query the database, grab some details and write them to a config file, before the rest of my application is initialised.
See how I'm able to access the value in a route?
// app/Providers/ConfigServiceProvider.php
namespace App\Providers;
use App\Models\Country;
use Illuminate\Support\ServiceProvider;
class ConfigServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
$countries = Country::pluck('name', 'iso_code');
config()->set(['app.countries' => $countries->toArray()]);
}
/**
* Register any application services.
*
* #return void
*/
public function register()
{
//
}
}
// config/app.php
// ...
'providers' => [
// ...
App\Providers\ConfigServiceProvider::class,
// ...
],
// ...
// routes/web.php
Route::get('config', function () {
dd(config('app.countries'));
});
You really should not want to do this. Initialising DB connections from a model requires all config to be loaded, and you intend to use these connections to define your config. You've found yourself having a circular dependency problem.
I'd like to suggest having a look at the socialite package you're trying to use. If no facilities exist in the service to set/override credentials at runtime, see if you're able to extend the service with your own implementation that does allow for that. I think that will be the only way to accomplish what you're trying to do.
Besides all that, config files are supposed to be composed of only scalar values and arrays, so that they can be cached.
I have a multiauth laravel 5.2 app, with the fallowing guards defined on config/auth.php:
...
'admin' => [
'driver' => 'session',
'provider' => 'admin',
],
'user' => [
'driver' => 'session',
'provider' => 'user',
],
...
So, admin and user.
The problem resides in the view layer, since this two loggedin guards share some views, ex:
Hello {{Auth::guard('admin')->user()->name}}
In this case the guard is hardcoded into the view to be always admin (it gives error when loggedin guard is user), but, to avoid have to do another equal view just for this little change, I would like have it dinamic, something like:
Hello {{Auth::guard(<LOGGEDIN GUARD>)->user()->name}}
PS: I know that this could be achieved getting the corresponding url segment, ex: www.site.com/pt/user/dasboard which in the case it would be segment 2, but this way the app would lose scalability, since in the future the corresponding segment may not be the same (2 in the example above)
One way to do this is to extend the Laravel authentication class in the IoC container to include, for instance, a name() method that check which guard is used for the current session, and calls user() on that Guard instance.
Another way is to simply use an if-statement in your Blade template:
#if(Auth::guard('admin')->check())
Hello {{Auth::guard('admin')->user()->name}}
#elseif(Auth::guard('user')->check())
Hello {{Auth::guard('user')->user()->name}}
#endif
However, this is a little dirty. You can clean this up a bit by using a partial, or by passing the view a variable containing the guard name, either directly from your Controller, or via a ViewComposer, and then doing:
Hello {{Auth::guard($guardName)->user()->name}}
in your View.
Extending Laravel's authentication is your best option, imo.
This will get the guard name that is used for current logged in user
Auth::getDefaultDriver()
When you log in, by default it will get you the:
'web'
Dependable through which guard you've been logged in it will get you that guard name.
This is not applicable for APIs!!! Because APIs in laravel by default don't use session.
Since Laravel 5.5, this is easy to do with the #auth template directive.
#auth("user")
You're a user!
#endauth
#auth("admin")
You're an administrator!
#endauth
#guest
You're not logged in!
#endguest
Reference: https://laravel.com/docs/5.6/blade#if-statements
In new versions of laravel use:
Auth::getDefaultDriver()
I recommend to use global helper function like
function activeGuard(){
foreach(array_keys(config('auth.guards')) as $guard){
if(auth()->guard($guard)->check()) return $guard;
}
return null;
}
Depends of Harat answer, I built a Class names CustomAuth, and it give me easy access to Auth facade methods: user() and id().
<?php
namespace App\Utils;
use Illuminate\Support\Facades\Auth;
class CustomAuth{
static public function user(){
return Auth::guard(static::activeGuard())->user() ?: null;
}
static public function id(){
return static::user()->MID ?: null;
}
static private function activeGuard(){
foreach(array_keys(config('auth.guards')) as $guard){
if(auth()->guard($guard)->check()) return $guard;
}
return null;
}
}
using auth()->guard($guard)->getName() will return two different type of values
login_admin_59ba36addc2b2f9401580f014c7f58ea4e30989d
if is an admin guard
login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d
if is a web guard or depending on your use case a user guard. So you can test against that.
so a simple use case can be as stated below
if(str_contains(auth()->guard($guard)->getName(), 'admin')){
dd('is admin');
}
here if is an admin, it will show 'is admin' otherwise you get the default
Auth::getDefaultDriver()
Above will return the current guard, in your case (user or admin)
In my case I had up and running project with too often usage of auth()->user() thus I was obligated to find a way to keep using multiple guards across my app.
Use a middleware to handle overwriting the value the default guard.
Add a custom web middleware to your web group within kernel.php
protected $middlewareGroups = [
'web' => [
.....
//This cool guy here
\App\Http\Middleware\CustomWebMiddleware::class,
],
Use CustomWebMiddleware.php content:
<?php
namespace App\Http\Middleware;
use Closure;
class CustomWebMiddleware
{
public function handle($request, Closure $next)
{
$customGuard = 'custom-guard-name-goes-here';
if (auth($customGuard)->check()) {
config()->set('auth.defaults.guard', $customGuard);
}
// if you have multiple guards you may use this foreach to ease your work.
/*$guards = config('auth.guards');
foreach ($guards as $guardName => $guard) {
if ($guard['driver'] == 'session' && auth($guardName)->check()) {
config()->set('auth.defaults.guard', $guardName);
}
}*/
return $next($request);
}
}
I'm displaying a front-end countdown of when the user's session will expire, I want to request the time left without updating it.
Here's what I have so far:
$ttl = Redis::ttl(config('cache.prefix') . ':' . Session::getId());
return response()->json($ttl);
Each time this is requested the ttl is reset back to the session.lifetime value.
I solved this by extending the StartSession middleware:
class StartSession extends \Illuminate\Session\Middleware\StartSession
{
public function terminate($request, $response)
{
if (!$request->is('auth/ping')) {
parent::terminate($request, $response);
}
}
}
Where auth/ping is the route I don't want the session to save on.
I then registered this in the app container as a singleton, so the terminate method resolves to the same instance:
In AppServiceProvider->register:
$this->app->singleton('App\Http\Middleware\StartSession');
The existing mapWebRoutes() method in app/Providers/RouteServiceProvider.php looks like this.
/**
* Define the "web" routes for the application.
*
* These routes all receive session state, CSRF protection, etc.
*
* #param \Illuminate\Routing\Router $router
* #return void
*/
protected function mapWebRoutes(Router $router)
{
$router->group([
'namespace' => $this->namespace, 'middleware' => 'web',
], function ($router) {
require app_path('Http/routes.php');
});
}
You can simply add something like the following to that method, or you could duplicate the code above which loads routes.php and remove the web middleware.
$router->get('session-ttl', function () {
return response()->json(
\Redis::ttl(config('cache.prefix') . ':' . cookie(config('session.cookie')));
);
});
Or
$router->group([
'namespace' => $this->namespace
], function ($router) {
require app_path('Http/routes_wo_web.php');
});
The session stuff is handled by the Illuminate\Session\Middleware\StartSession middleware, which is in the web middleware group.
The simplest solution would be to place your route outside the standard web middleware group.
If you need any of the other web middlewares, you can add them back to the route.
To actually use the session in your route, you have a couple of options:
you can make a Session middleware that does not terminate the session (extend the existing one, and override the handle method to just return $next($request))
someone here's suggested that as it's a one off route, you could just start the session manually wherever you're handling your route (nick the code from the existing middleware - it's only a few lines).
I think the recommended solution would be to do it in a middleware, though.
Recently I've been trying to learn to work with Laravel. Most tutorials are in 4.*, but that's okay. Implementing/converting some deprectated functions are going fine until now. I found out that version 5.* has deprecated the beforeFilter as in:
public function __construct() {
$this->beforeFilter('csrf', array('on' => ['post', 'put', 'patch', 'delete']));
}
I want to convert this to version 5.*. From what I understand this can be done with Middleware, but I have no idea how I can achieve the same result. I have read the docs, but this didn't really help me understand the topic.
There already is a middleware file called VerifyCsrfToken.php in the app/Http/Middleware folder with this code:
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* The URIs that should be excluded from CSRF verification.
*
* #var array
*/
protected $except = [
//
];
}
Can anyone guide me to set this up and help me understand Middleware a bit better? Thank you.
Because CSRF protection is something that Laravel 5 comes bundled with, this is actually something it checks by default within the Illuminate\Foundation\Http\Middleware\VerifyCsrfToken class that you see being extended in VerifyCsrfToken.php.
If you have a look in the handle method of that class, you'll see that the first condition that would make the verification successful, calls the isReading method which looks like this:
/**
* Determine if the HTTP request uses a ‘read’ verb.
*
* #param \Illuminate\Http\Request $request
* #return bool
*/
protected function isReading($request)
{
return in_array($request->method(), ['HEAD', 'GET', 'OPTIONS']);
}
This does the equivalent of what your beforeFilter did in Laravel 4, thus allowing the request to execute for "read" verbs and automatically verifying the token if any other verbs are used, such as post, put, patch, delete.
If you check the Laravel CSRF Protection Documentation you'll see there's one paragraph that reads:
You do not need to manually verify the CSRF token on POST, PUT, or DELETE requests. The VerifyCsrfToken HTTP middleware will verify that the token in the request input matches the token stored in the session.
So there's no more need for you to have that filter. As for understanding how Middleware works in Laravel, reading the entire HTTP Middleware Documentation will do a great job of helping you figure out how it works.