Auth & Preflight Middleware

Hi All

I hope someone can help, I am struggling to add some middleware (I think)
I have created the below api via the docs and various tutorials that talks to a mySQL & SQL server database.
Problems: I am currently repeating a lot of code and wondering if anyone knows how I can prevent it.

Problem 1 (Auth): See CompanyController.php & Validator.php
For every function in a controller I am writing the same auth logic and hope someone could help prevent that.
I have seen and attempted to add middleware to a group but cannot get anything to work.
I would like to have on a group or single route an authorization check: See Validator.php below.

Problem 2 (Preflight): See Routes.php & PreFlightController.php
For every route I am having to to add an options route so it works through a browser, Is there a way of adding this PreFlightController to all routes in the $app?

I hope I have made sense, any help would be very much appreciated.
(I have added my questions in the code comments)

Sorry its a lot.

Thanks

**MAIN API CODE BELOW ** (Questions prefixed with //???..)

bootstrap.php

<?php
date_default_timezone_set('Europe/London');
use DI\Container;
use DI\Bridge\Slim\Bridge as SlimAppFactory;

require_once __DIR__  .'/../vendor/autoload.php';
require_once __DIR__  .'/../vendor/paragonie/sodium_compat/autoload.php';

$container = new Container();

$settings = require_once __DIR__.'/settings.php';
$settings($container);

$app = SlimAppFactory::create($container);
$app->setBasePath('/api');

$middleware = require_once __DIR__ . '/middleware.php';
$middleware($app);

$routes = require_once  __DIR__ .'/routes.php';
$routes($app);

$app->run();

settings.php

<?php
use Psr\Container\ContainerInterface;

return function (ContainerInterface $container)
{
  $container->set('settings',function()
  {
    $db = require __DIR__ . '/database.php';

    return [
        "db"=>$db
    ];
  });
};

middleware.php

<?php
use Slim\App;
use App\Response\CustomResponse;

use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Routing\RouteContext;

return function (App $app) {
    $app->getContainer()->get('settings');
    $app->addBodyParsingMiddleware();

    // This middleware will append the response header Access-Control-Allow-Methods with all allowed methods
    $app->add(function (Request $request, RequestHandlerInterface $handler): Response {
        $routeContext = RouteContext::fromRequest($request);
        $routingResults = $routeContext->getRoutingResults();
        $methods = $routingResults->getAllowedMethods();
        $requestHeaders = $request->getHeaderLine('Access-Control-Request-Headers');

        $response = $handler->handle($request);
        $response = $response->withHeader('Access-Control-Allow-Origin', '*');
        $response = $response->withHeader('Access-Control-Allow-Methods', implode(',', $methods));
        $response = $response->withHeader('Access-Control-Allow-Headers', $requestHeaders);

        return $response;
    });

    $app->addRoutingMiddleware();

    // Define Custom Error Handler
    $customErrorHandler = function (
        Request $request,
        Throwable $exception,
        bool $displayErrorDetails,
        bool $logErrors,
        bool $logErrorDetails
        // ?LoggerInterface $logger = null
    ) use ($app) {
        // $logger->error($exception->getMessage());
        $payload = ['error' => $exception->getMessage()];
        $response = $app->getResponseFactory()->createResponse();
        $customResponse = new CustomResponse();
        return $customResponse->is400Response($response, $payload);
    };
    $errorMiddleware = $app->addErrorMiddleware(true, true, true);
    $errorMiddleware->setDefaultErrorHandler($customErrorHandler);
};

database.php

<?php
//MY SQL
$database_config_mysql = [
 'driver'=>'mysql',
    'host'=>'localhost',
    'database'=>'mysql_dbname',
    'username'=>'username',
    'password'=>'password',
    'charset'=>'utf8',
    'collation'=>'utf8_unicode_ci',
    'prefix'=>''
];

//SQL SERVER
$database_config_sqlsrv = [
    'driver' => 'sqlsrv',
    'host' => 'sqlServerHost',
    'database' => 'sqlsrvl_dbname',
    'username' => 'username',
    'password' => 'password',
    'charset' => 'utf8',
    'collation' => 'utf8_unicode_ci',
    'prefix' => ''
];

$capsule = new Illuminate\Database\Capsule\Manager;
$capsule->addConnection($database_config_mysql);

//???How do I get access to this connection from one of my controllers???
$capsule->addConnection($database_config_sqlsrv , "SQLSRV");
$capsule->setAsGlobal();
$capsule->bootEloquent();

return $capsule;

routes.php

<?php
use Slim\App;
use Slim\Exception\HttpNotFoundException;

use App\Controllers\PreFlightController;
use App\Controllers\SQLSRV\CompanyController;

return function (App $app) {

    //???How do I have all of these routes go through my validation check before accessing the route.
    $app->group("/company", function ($app) {
            $app->get('/company', [CompanyController::class, 'getAllRows']);
            $app->get('/compay/{id}', [CompanyController::class, 'getSingleCompany']);
            $app->get('/company/search/{id}', [CompanyController::class, 'getAllRows']);
            
            //???Can I prevent writing an option route for each above route.
            $app->options('/company', [PreFlightController::class, 'preflight']);
            $app->options('/compay/{id}'', [PreFlightController::class, 'preflight']);
            $app->options('/company/search/{id}'', [PreFlightController::class, 'preflight']);
    });

    $app->map(['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], '/{routes:.+}', function ($request, $response) {
        throw new HttpNotFoundException($request);
    });
};

CompanyController.php (sqlsrv)

<?php
namespace  App\Controllers\SQLSRV;

use App\Response\CustomResponse;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use App\Models\SQLSRV\Company;
use App\Validation\Validator;

class CompanyController
{
    protected $company;
    protected $customResponse;
    protected $validator;
    public function __construct()
    {
        $this->company= new Company();
        $this->customResponse = new CustomResponse();
        $this->validator = new Validator();
    }

    public function getCompanies(Request $request, Response $response)
    {

        //???Can I prevent writing this validation logic in every function, Would like to add it to all routes in a group.
        $user= $this->validator->validateFirebaseJWT($request);
        if ($user== null) {
            return $this->customResponse->is401Response($response, "Unauthorised Request");
         }

        $users = $this->user->get();
        return $this->customResponse->is200Response($response, $users);
    }

//???How can I run a stored procedure on a SQL server?
 public function runStoredProcedure(Request $request, Response $response)
    {
        //???I need a connection to the SQLSRV database, something like below...
        $result = $db->select($db->raw("Stored Procedure Code'") 
        return $this->customResponse->is200Response($response, $result );
    }

}

Company.php (sqlsrv)

<?php
namespace App\Models\Eclipse;

use Illuminate\Database\Eloquent\Model;

class Company extends Model
{
    protected $table = 'SQLSRV.Commercial.Company';
    protected $fillable = [
        "CompanyID",
        "CompanyName",
    ];
}

validator.php

<?php

namespace  App\Controllers\Base;

use Firebase\JWT\JWT;
use Psr\Http\Message\RequestInterface as Request;
use App\Models\User;
use App\Models\GooglePublicKey;

//Gets the GooglePublicKey stored in the database
//Firebase\JWT\JWT Decodes the JWT
//Use Decoded data to check user is in the data base
//If user then authorize

//NOTE:this has some functions removed for space
class FirebaseAuth
{
    protected $user;
    protected $googlePublicKey;

    public function __construct()
    {
        $this->user = new User();
        $this->googlePublicKey = new GooglePublicKey();
    }

 public function validateFirebaseJWT(Request $request)
    {
        //Get JWT from auth header
        $jwt = $this->getJWT($request);
        if (!$jwt) {
            return null;
        }
        //Check if use is found in database
        $userData = $this->getUserData($jwt);
        if (!$userData) {
            return null;
        } else {
            return $userData;
        }
    }
}

PreFlightController.php

<?php
namespace  App\Controllers;

use Psr\Http\Message\ResponseInterface as Response;

class PreFlightController
{
    public function preflight(Response $response)
    {
        return $response;
    }
}

CustomResponse.php

<?php

namespace  App\Response;

class CustomResponse
{

    public function is200Response($response, $responseMessage)
    {
        $responseMessage = json_encode(["success" => true, "response" => $responseMessage, "status" => 200]);
        $response->getBody()->write($responseMessage);
        return $response->withHeader("Content-Type", "application/json")
            ->withStatus(200);
    }

    public function is400Response($response, $responseMessage)
    {
        $responseMessage = json_encode(["success" => false, "response" => $responseMessage, "status" => 400]);
        $response->getBody()->write($responseMessage);
        return $response->withHeader("Content-Type", "application/json")
            ->withStatus(400);
    }

    public function is401Response($response, $responseMessage)
    {
        $responseMessage = json_encode(["success" => false, "response" => $responseMessage, "status" => 401]);
        $response->getBody()->write($responseMessage);
        return $response->withHeader("Content-Type", "application/json")
            ->withStatus(401);
    }
}

I try to give you some general tips.

To prevent code duplication, you may move the shared logic into a specific class. Depending on the use case, this class could be a Middleware class or a Service class.

According to CORS, I would choose a middleware based approach, because defining OPTION route for each route is a lot of extra work.

Example of a generic CorsPreflightMiddleware:

if ($request->getMethod() === 'OPTIONS') {
    return $this->responseFactory->createResponse();
}

return $handler->handle($request);

The CorsPreflightMiddleware should be added after the RoutingMiddleware.

$app->addRoutingMiddleware();
$app->add(\App\Middleware\CorsPreflightMiddleware::class);

To send the correct CORS headers you also need to replace Slim’s default emitter with a custom CorsResponseEmitter. Example.

Usage example in index.php

Thanks for the reply,

Seeing the ‘OPTIONS’ gave me an idea for the Preflight problem, I added the following to the routes.php which seems to work.

    $app->map(['OPTIONS'], '/{routes:.+}', function ($request, $response) {
        return $response;
    });

I still cant seem to add middleware to a route or group, I am taking examples from the docs but getting the following errors:

"Too few arguments to function Closure::{closure}(), 2 passed in C:\\xampp\\htdocs\\foukAPI\\vendor\\slim\\slim\\Slim\\MiddlewareDispatcher.php on line 313 and exactly 3 expected"
$app->group("/users", function ($app) {
        $app->get('/users', [UserController::class, 'getAllRows']);
    })->add(function ($request, $response, $next) {
        $user = ['name' => 'Joe'];
        if ($user) {
            $response = $next($request, $response);
        } else {
            $response->getBody()->write('Unauthorized');
        }

        return $response;
    });
"Call to undefined method Slim\\MiddlewareDispatcher::getBody()"
    $app->group("/users", function ($app) {
        $app->get('/users', [UserController::class, 'getAllRows']);
    })->add(function ($request, $response) {
        $response->getBody()->write('Hello');
        return $response;
    });

Any Ideas?

Thanks

Do you use Slim 3 or Slim 4?

Hi
I am using Slim4

Thanks

In this case you may add a middleware as documented in the Slim 4 docs, see here.