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

hangra's avatar

Make API only accessible from frontend (no direct access)

Hi everyone,

My simple web application consists of a Laravel API as backend and a React App as frontend. The application is accessible through https://example.com and makes requests to the API via https://api.example.com. The API should only be called by the frontend and should not be accessed directly.

For example my frontend calls https://api.example.com/items to fetch all items. However, it should NOT be possible for users to directly access the endpoint in their browser via https://api.example.com/items. So, requests to the API endpoints should only be possible through the frontend.

I have already installed and configured Sanctum and updated the CORS configuration in config/cors.php. Unfortunately, I don't no which further configuration steps are needed since I don't need the user authentication.

So, is there a way to secure my endpoints without implementing authentication?

0 likes
10 replies
vincent15000's avatar

Where have you declared your api routes ? In the api.php file or in the web.php file ?

Furthermore your API routes should be protected by the sanctum middleware like this.

Route::middleware('auth:sanctum')->group(function () {
	...
});
hangra's avatar

@vincent15000 I have declared the following routes in api.php:

Route::get('/', function () {
    abort('403');
});

Route::middleware('auth:sanctum')->resource('items', ItemsController::class);

In addition, I have done all steps described here: https://laravel.com/docs/9.x/sanctum#spa-authentication apart from the login process.

Now I can neither access the api from the React frontend, nor directly from the backend api…

hangra's avatar

@vincent15000

Yes, but as I said I now can't access it from frontend too.

My config/cors.php looks like this:

'paths' => ['*', 'sanctum/csrf-cookie'],

    'allowed_methods' => ['*'],

    'allowed_origins' => ['*'],

    'allowed_origins_patterns' => [],

    'allowed_headers' => ['*'],

    'exposed_headers' => [],

    'max_age' => 0,

    'supports_credentials' => true,

And config/sanctum.php looks like this:

	'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
        '%s%s',
        'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
        env('APP_URL') ? ',' . parse_url(env('APP_URL'), PHP_URL_HOST) : ''
    ))),

    /*
    |--------------------------------------------------------------------------
    | Sanctum Guards
    |--------------------------------------------------------------------------
    |
    | This array contains the authentication guards that will be checked when
    | Sanctum is trying to authenticate a request. If none of these guards
    | are able to authenticate the request, Sanctum will use the bearer
    | token that's present on an incoming request for authentication.
    |
     */

    'guard' => ['web'],

    /*
    |--------------------------------------------------------------------------
    | Expiration Minutes
    |--------------------------------------------------------------------------
    |
    | This value controls the number of minutes until an issued token will be
    | considered expired. If this value is null, personal access tokens do
    | not expire. This won't tweak the lifetime of first-party sessions.
    |
     */

    'expiration' => null,

    /*
    |--------------------------------------------------------------------------
    | Sanctum Middleware
    |--------------------------------------------------------------------------
    |
    | When authenticating your first-party SPA with Sanctum you may need to
    | customize some of the middleware Sanctum uses while processing the
    | request. You may change the middleware listed below as required.
    |
     */

    'middleware' => [
        'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
        'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
    ],

In my .env I changed the following:

APP_URL=https://api.example.com
SESSION_DRIVER=cookie
SESSION_DOMAIN=.example.com
SANCTUM_STATEFUL_DOMAINS=example.com
vincent15000's avatar

@hangra For me, example.com is different from localhost:8000 and localhost:3000.

In order to authenticate, your SPA and API must share the same top-level domain. However, they may be placed on different subdomains. Additionally, you should ensure that you send the Accept: application/json header with your request.
martinbean's avatar

@hangra You can’t magically make an API work from one request and block it from others.

You can use things like CORS to restrict which origins can make requests to your API via JavaScript, but that won’t stop someone from being able to hit your API from a server, or cURL, or a tool like Postman, etc.

1 like
vincent15000's avatar

@martinbean I have written an API and I can't access it another way than from the frontend in VueJS. Even if I loggin on the backend, I can't access it via the URL, it automatically redirects to a specific page.

vincent15000's avatar

Here is my configuration.

// CORS
'paths' => ['api/*', 'sanctum/csrf-cookie', 'login', 'logout'],
'allowed_methods' => ['*'],
'allowed_origins' => ['http://localhost:8080', 'http://a.b.c.d:port'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
// API ROUTES
Route::middleware('auth:sanctum')->group(function () {
	...
});
// WEB ROUTES
Route::redirect('/', 'login');
Route::middleware(['auth'])->group(function () {
	Route::get('tokens', [TokenController::class, 'index'])->name('tokens.index');
	Route::post('tokens', [TokenController::class, 'create'])->name('tokens.create');
	Route::delete('tokens/{tokenId}', [TokenController::class, 'destroy'])->name('tokens.delete');
});
vincent15000's avatar

@hangra That will not help you. It's just to list, create or delete tokens. Well ... classical CRUD.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class TokenController extends Controller
{

    public function index(Request $request)
    {
        $tokens = auth()->user()->tokens;
        return view('tokens.index', compact('tokens'));
    }

    public function create(Request $request)
    {
        $token = $request->user()->createToken('mytoken');
        return redirect()->route('tokens.index')->with('token', $token->plainTextToken);
    }

    public function destroy($tokenId)
    {
        auth()->user()->tokens()->where('id', $tokenId)->delete();
        return redirect()->route('tokens.index');
    }

}

Please or to participate in this conversation.