Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

angerfist's avatar

ip block city

how to deny access to a site from countries, is there a ready solution on laravel 9?

0 likes
57 replies
Sinnbeck's avatar

@angerfist Its just a simple middleware class that I have added to the web array in /app/Http/Kernel.php

I added the countries to to a config file which reads them from an env file :)

<?php

namespace App\Http\Middleware;

use App\Http\Middleware\Traits\CheckByIp;
use Closure;
use Illuminate\Support\Facades\Log;
use Stevebauman\Location\Facades\Location;

class RestrictByCountry
{
    protected $except = [
        'employment/*',
        'geo',
        'webhooks/*',
    ];

    /**
     * Handle an incoming request.
     *
     * @param \Illuminate\Http\Request $request
     * @param \Closure                 $next
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        if (!app()->environment('production') || $this->inExceptArray($request)) {
            return $next($request);
        }

        $position = Location::get($request->ip());
        $access = $this->accessByCountry($position);

        if (!$access) {
            abort(405, 'Not allowed');
        }

        return $next($request);
    }

    /**
     * Determine if the request has a URI that should pass through CSRF verification.
     *
     * @param \Illuminate\Http\Request $request
     *
     * @return bool
     */
    protected function inExceptArray($request)
    {
        foreach ($this->except as $except) {
            if ($except !== '/') {
                $except = trim($except, '/');
            }

            if ($request->fullUrlIs($except) || $request->is($except)) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return bool
     */
    protected function accessByCountry($position): bool
    {
        $allowed_countries = config('auth.countries');
        $country = $position->countryName;

        return in_array($country, $allowed_countries);
    }

}
1 like
Sinnbeck's avatar

@angerfist something like

ALLOWED_COUNTRIES="Sweden Germany"

And in the auth.php config file

'countries' => env(explode(' ', 'ALLOWED_COUNTRIES'), []), 
angerfist's avatar

@Sinnbeck Thank you very much, I'll try it, if everything goes well I put it as the best answer

Sinnbeck's avatar

@angerfist ok. Be aware that my solution is for countries. You mentioned both countries and cities so I am unsure which you are after. But be aware that city won't be reliable

angerfist's avatar

@Sinnbeck here is my kernel.php how to edit it correctly for this?

  1. Made composer require stevebauman/location
  2. php artisan vendor:publish --provider="Stevebauman\Location\LocationServiceProvider"
  3. I started to do as you have here, as a result, a white screen, changed the country to my own. Need your help, please help
<?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<int, class-string|string>
     */
    protected $middleware = [
        // \App\Http\Middleware\TrustHosts::class,
        \App\Http\Middleware\TrustProxies::class,
        \Illuminate\Http\Middleware\HandleCors::class,
        \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array<string, array<int, class-string|string>>
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::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<string, class-string|string>
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::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,
    ];
}
Sinnbeck's avatar

@angerfist You add the middleware to the web array

    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
            \App\Http\Middleware\RestrictByCountry::class, //here
        ],

        'api' => [
            // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
            'throttle:api',
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],
    ];

And I don't know what you mean by a white screen. That normally assumes you are returning an empty response somewhere.

angerfist's avatar

@Sinnbeck my config/auth.php

<?php

return [

    /*
    |--------------------------------------------------------------------------
    | Authentication Defaults
    |--------------------------------------------------------------------------
    |
    | This option controls the default authentication "guard" and password
    | reset options for your application. You may change these defaults
    | as required, but they're a perfect start for most applications.
    |
    */

    'defaults' => [
        'guard' => 'web',
        'passwords' => 'users',
    ],

    /*
    |--------------------------------------------------------------------------
    | Authentication Guards
    |--------------------------------------------------------------------------
    |
    | Next, you may define every authentication guard for your application.
    | Of course, a great default configuration has been defined for you
    | here which uses session storage and the Eloquent user provider.
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | Supported: "session"
    |
    */

    'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | User Providers
    |--------------------------------------------------------------------------
    |
    | All authentication drivers have a user provider. This defines how the
    | users are actually retrieved out of your database or other storage
    | mechanisms used by this application to persist your user's data.
    |
    | If you have multiple user tables or models you may configure multiple
    | sources which represent each model / table. These sources may then
    | be assigned to any extra authentication guards you have defined.
    |
    | Supported: "database", "eloquent"
    |
    */

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],

        // 'users' => [
        //     'driver' => 'database',
        //     'table' => 'users',
        // ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Resetting Passwords
    |--------------------------------------------------------------------------
    |
    | You may specify multiple password reset configurations if you have more
    | than one user table or model in the application and you want to have
    | separate password reset settings based on the specific user types.
    |
    | The expire time is the number of minutes that each reset token will be
    | considered valid. This security feature keeps tokens short-lived so
    | they have less time to be guessed. You may change this as needed.
    |
    */

    'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

    /*
    |--------------------------------------------------------------------------
    | Password Confirmation Timeout
    |--------------------------------------------------------------------------
    |
    | Here you may define the amount of seconds before a password confirmation
    | times out and the user is prompted to re-enter their password via the
    | confirmation screen. By default, the timeout lasts for three hours.
    |
    */

    'password_timeout' => 10800,

    'countries' => env(explode(' ', 'ALLOWED_COUNTRIES'), []),
];
angerfist's avatar

@Sinnbeck

APP_NAME=Laravel
APP_ENV=production
APP_KEY=*********************
APP_DEBUG=false
APP_URL=https://site.com

LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=****
DB_USERNAME=***
DB_PASSWORD=****

BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120

MEMCACHED_HOST=127.0.0.1

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_MAILER=smtp
MAIL_HOST=mailhog
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="[email protected]"
MAIL_FROM_NAME="${APP_NAME}"

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1

MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"

HONEYPOT_NAME=honeypot_for_spam
ALLOWED_COUNTRIES='russia'
Sinnbeck's avatar

None. What do you think you are missing? Still getting a white screen?

Sinnbeck's avatar

@angerfist nothing is needed. But remove one thing at a time again until you get an error instead of a white screen. I use this exact code and have no such errors

Sinnbeck's avatar

@angerfist I have you 4 changes. 3 edits to files and adding a new file. Remove those changes one by one until you are back to your site working.

Sinnbeck's avatar

@angerfist Ok. Go back to before you started then. I assume you have a backup or can reset with git?

Tray2's avatar

@angerfist Why have you set production in your env-file? That should only be set in a production environment. If you are doing changes in production.

STOPP IMMEDIATELY!!

Laravel doesn't show any errors when in production mode, you need to check the logs from the web server. They will tell you what you are doing wrong.

Sinnbeck's avatar

Nice spot by @tray2. If you set it to production locally to test it out it will of course not work.. Your local server will not get a russian IP. Also be sure that you of course set up location.php for that package. I didnt give you instructions for that, as those are already on the page for the package.. This is not a tutorial

You might want to set up this part with a russian IP if you want to test it locally https://github.com/stevebauman/location/blob/master/config/location.php#L171

Sinnbeck's avatar

@angerfist So you are changing your production site? Ouch! Did you set up that package at least? I personally use maxmind for my sites.

angerfist's avatar

@Sinnbeck I have a production site, I can't use maxmind because there is no Russia there

angerfist's avatar

@Sinnbeck nothing works for me, I don’t know what to do anymore, I’ve tried everything

Sinnbeck's avatar

@angerfist If there isnt a single of those providers that can detect a Russian IP then I dont know how I can be of any help?

Did you try typing in your ip at https://ip-api.com/ to see if that can find it?

Sinnbeck's avatar

@angerfist I just found a random russian ip and ran it through maxmind, and it came back with Russia? So I dont see why you cant use that?

angerfist's avatar

@Sinnbeck nothing comes out, I don’t know how to write a driver, I asked for help here, but it’s all in vain in general. I asked you step by step how to set everything up

Sinnbeck's avatar

@angerfist Sorry but I dont feel like writing a complete tutorial for this. Maybe I will at a later point, for my blog. But I have literally given you all the code needed to set this up. Sounds like all you need is to download the max mind database from their website and point to it. I put mine in /storage/geoip

    'maxmind' => [

        'web' => [

            'enabled' => false,

            'user_id' => env('GEOIP_MAXMIND_USER_ID'),

            'license_key' => env('GEOIP_MAXMIND_LICENSE_KEY'),

            'options' => [

                'host' => 'geoip.maxmind.com',

            ],

        ],

        'local' => [

            'type' => 'city',

            'path' =>storage_path('geoip/GeoLite2-City.mmdb'),

        ],

    ],

I am using the city version, but you can use the country version if you prefer. You point to that, and change the type

Tray2's avatar

@angerfist FFS, if you have no clue as to what you are doing, and you are bashing @sinnbeck who is trying to help you for free. Hire a developer to help you develop what you need.

3 likes
angerfist's avatar

@Tray2 I do not criticize him, on the contrary, I thank him for saying what leads me on the right path

angerfist's avatar

@Sinnbeck right ?

    /*
    |--------------------------------------------------------------------------
    | Driver
    |--------------------------------------------------------------------------
    |
    | The default driver you would like to use for location retrieval.
    |
    */

    // 'driver' => Stevebauman\Location\Drivers\IpApi::class,
       'driver' => Stevebauman\Location\Drivers\Ipdata::class,
Tray2's avatar

@angerfist Yes you are, you want a solution for free and you are bagering him to help you, but you haven't provided a single error or any of the code you have written.

3 likes
migsAV's avatar

@Sinnbeck thank you so much for this, it's what one of my apps needed.

I found that this needed to be changed

'countries' => env(explode(' ', 'ALLOWED_COUNTRIES'), []), 

because it was giving me this error

($name) must be of type string, array given, called in /var/www/html/vendor/laravel/framework/src/Illuminate/Support/Env.php on line 76

so I changed it to this on my side

'countries' => explode(', ',  env('ALLOWED_COUNTRIES')),

Side Note

Another thing I had to change was

ALLOWED_COUNTRIES="Sweden Germany"

because I live in South Africa it includes two words so I just made the explode separator , instead of an empty space

Sinnbeck's avatar

@migsAV Ah yes that makes sense :) Great that you got it working. Never thought that it could be 2 words :p

1 like
jaseofspades88's avatar

People like @sinnbeck really are what give this community its backbone. How often do we see people on this forum, level 1, not subscribed, no videos watched not really reading the advice they're being given but simply expecting to copy and paste a response into their application and it just work?

4 likes
martinbean's avatar

thanks man. Appreciate that 😁

@Sinnbeck I’m not sure that was meant as a compliment…

I think @jaseofspades88 was pointing out that @angerfist posted a vague question and then their first reply was:

tell me in detail how to implement all this on Laravel 9, please

Even though they’ve not tried anything themselves, and have so far only watched a mere 8 videos on Laracasts. Entitled, much?

So I think they were just point out that by putting so much effort into going back and forth with them, spoon-feeding them every line of code they need to write, is just enabling more of these types of posters and questions in the future.

I’m all for helping people, but I draw the line at entitled people like the OP who just want an entire solution written for them for free so they can copy and paste it, and then come back at a later date when they have another feature they want to add to their project.

1 like
Sinnbeck's avatar

@martinbean that might be so. Maybe I misunderstood. 🤷‍♂️ I thought being a backbone was a good thing, but English isn't my first language

But each has their own line I suppose. I do my best to help and maybe I go too far. But if people want a strict forum where an exact question is asked the first time then maybe laracasts isn't the right place. I see laracasts as a place where people help each other any way they can.

2 likes
jaseofspades88's avatar

@martinbean what if I told you it was simultaneously a compliment to @sinnbeck and a gripe at the people you have detailed above. One thing I love about the work the Laracasts team and a lot of the people who put their time and effort into helping others do, is give you the helping hand you need to then go and learn more, ask questions and improve

3 likes

Please or to participate in this conversation.