I'm writing a REST API with Slim. I have written a small middleware to protect the resources so only authenticated users will be able to access them:
<?php
class SecurityMiddleware extends \Slim\Middleware
{
protected $resource;
public function __construct($resource)
{
$this->resource = $resource;
}
public function call()
{
//get a reference to application
$app = $this->app;
//skip routes that are exceptionally allowed without an access token:
$publicRoutes = ["/","/login","/about"];
if (in_array($app->request()->getPathInfo(),publicRoutes)){
$this->next->call(); //let go
} else {
//Validate:
if ($this->resource->isValid()){
$this->next->call(); //validation passed, let go
} else {
$app->response->setStatus('403'); //validation failed
$app->response->body(json_encode(array("Error"=>"Access token problem")));
return;
}
}
}
}
This works, but the undesired side effect is the middleware does not make a distinction between existing routes and non-existing routes. For example, if a the user attempts to request a route like /dfghdfgh which does not exist, instead of getting an HTTP status code of 404 he'll get a 403 saying there is no access token. I would like to add an implementation similar to the following check on the middleware class:
if ($app->hasRoute($app->request->getPathInfo()){
$this->next->call(); //let go so user gets 404 from the app.
}
Any ideas how this can be achieved?
I use a hook to do what you're trying to do, as MamaWalter suggested, but you want to use slim.before.dispatch rather than an earlier hook. If the route your user is trying to visit doesn't exist, the hook will never be called and the 404 gets thrown.
I'm doing exactly that in my own Authorization Middleware. Works like a charm.
Maybe my implementation will work for you:
<?php
class CustomAuth extends \Slim\Middleware {
public function hasRoute() {
$dispatched = false;
// copied from Slim::call():1312
$matchedRoutes = $this->app->router->getMatchedRoutes($this->app->request->getMethod(), $this->app->request->getResourceUri());
foreach ($matchedRoutes as $route) {
try {
$this->app->applyHook('slim.before.dispatch');
$dispatched = $route->dispatch();
$this->app->applyHook('slim.after.dispatch');
if ($dispatched) {
break;
}
} catch (\Slim\Exception\Pass $e) {
continue;
}
}
return $dispatched;
}
public function call() {
if ($this->hasRoute()) {
if ($authorized) {
$this->next->call();
}
else {
$this->permissionDenied();
}
}
else {
$this->next->call();
}
}
}
Not exactly what you asking for, but personnaly when i need to check authentification on some routes i do it like this.
config:
$config = array(
...,
'user.secured.urls' => array(
array('path' => '/user'),
array('path' => '/user/'),
array('path' => '/user/.+'),
array('path' => '/api/user/.+')
),
...
);
middleware:
/**
* Uses 'slim.before.router' to check for authentication when visitor attempts
* to access a secured URI.
*/
public function call()
{
$app = $this->app;
$req = $app->request();
$auth = $this->auth;
$config = $this->config;
$checkAuth = function () use ($app, $auth, $req, $config) {
// User restriction
$userSecuredUrls = isset($config['user.secured.urls']) ? $config['user.secured.urls'] : array();
foreach ($userSecuredUrls as $url) {
$urlPattern = '#^' . $url['path'] . '$#';
if (preg_match($urlPattern, $req->getPathInfo()) === 1 && $auth->hasIdentity() === false) {
$errorData = array('status' => 401,'error' => 'Permission Denied');
$app->render('error.php', $errorData, 401);
$app->stop();
}
}
};
$app->hook('slim.before.router', $checkAuth);
$this->next->call();
}
but if almost all your routes need authentification maybe not the best solution.
great example: http://www.slideshare.net/jeremykendall/keeping-it-small-slim-php
Related
I am trying to setup google indexing api in codeigniter, I have done all steps on google cloud and search console part.
It works, but returning success message on all options event when url is not submited, that is why I want to get exact response from google instead of a created success message.
How can I display exact response from google return $stringBody;? or check for the correct response ?
Here is my controller :
namespace App\Controllers;
use App\Models\LanguageModel;
use App\Models\IndexingModel;
class IndexingController extends BaseController
{
public function initController(\CodeIgniter\HTTP\RequestInterface $request, \CodeIgniter\HTTP\ResponseInterface $response, \Psr\Log\LoggerInterface $logger)
{
parent::initController($request, $response, $logger);
$this->indexingModel = new IndexingModel();
}
public function GoogleUrl()
{
checkPermission('indexing_api');
$data['title'] = trans("indexing_api");
$data["selectedLangId"] = inputGet('lang');
if (empty($data["selectedLangId"])) {
$data["selectedLangId"] = $this->activeLang->id;
}
echo view('admin/includes/_header', $data);
echo view('admin/indexing_api', $data);
echo view('admin/includes/_footer');
}
/**
* indexing Tools Post
*/
public function indexingToolsPost()
{
checkPermission('indexing_api');
$slug = inputPost('slug');
$urltype = inputPost('urltype');
$val = \Config\Services::validation();
$val->setRule('slug', trans("slug"), 'required|max_length[500]');
if (!$this->validate(getValRules($val))) {
$this->session->setFlashdata('errors', $val->getErrors());
return redirect()->to(adminUrl('indexing_api?slug=' . cleanStr($slug)))->withInput();
} else {
$this->indexingModel->AddUrlToGoogle($slug, $urltype);
$this->session->setFlashdata('success', trans("msg_added"));
resetCacheDataOnChange();
return redirect()->to(adminUrl('indexing_api?slug=' . cleanStr($slug)));
}
$this->session->setFlashdata('error', trans("msg_error"));
return redirect()->to(adminUrl('indexing_api?slug=' . cleanStr($slug)))->withInput();
}
}
And This is my model :
namespace App\Models;
use CodeIgniter\Model;
use Google_Client;
class IndexingModel extends BaseModel {
public function AddUrlToGoogle($google_url, $Urltype){
require_once APPPATH . 'ThirdParty/google-api-php-client/vendor/autoload.php';
$client = new Google_Client();
$client->setAuthConfig(APPPATH . 'ThirdParty/google-api-php-client/xxxxxxxxx.json');
$client->addScope('https://www.googleapis.com/auth/indexing');
$httpClient = $client->authorize();
$endpoint = 'https://indexing.googleapis.com/v3/urlNotifications:publish';
$array = ['url' => $google_url, 'type' => $Urltype];
$content = json_encode($array);
$response = $httpClient->post($endpoint,['body' => $content]);
$body = $response->getBody();
$stringBody = (string)$body;
return $stringBody;
}
public function AddUrlToBing($google_url, $Urltype){
}
public function AddUrlToYandex($google_url, $Urltype){
}
}
This is a success response when I try it out of codeigniter and print_r($stringBody);
{ "urlNotificationMetadata": { "url": "https://example.com/some-text", "latestUpdate": { "url": "https://example.com/some-text", "type": "URL_UPDATED", "notifyTime": "2023-01-29T01:51:13.140372319Z" } } }
And this is an error response :
{ "error": { "code": 400, "message": "Unknown notification type. 'type' attribute is required.", "status": "INVALID_ARGUMENT" } }
But In codeigniter I get a text message "url submited" even if url not submited.
Currently you are not handling the actual response of IndexingModel->AddUrlToGoogle(). It seems your code has a validation before, so it claims, if no validation error occurs, its always a success.
So the first question to ask is, why your validation is not working here - or is it?
Secondly you could handle the actual response in any case:
IndexingController
class IndexingController extends BaseController
public function indexingToolsPost()
{
if (!$this->validate(getValRules($val))) {
// validation error
$this->session->setFlashdata('errors', $val->getErrors());
return redirect()->to(adminUrl('indexing_api?slug=' . cleanStr($slug)))->withInput();
} else {
// no validation error
$apiResponseBody = $this->indexingModel->AddUrlToGoogle($slug, $urltype);
if(array_key_exists('error', $apiResponseBody)) {
// its an error!
// either set the actual messsage
$this->session->setFlashdata('error', $apiResponseBody['error']['message']);
// OR translate it
$this->session->setFlashdata('error', trans($apiResponseBody['error']['message']));
} else {
// Its a success!
$this->session->setFlashdata('success', trans("msg_added"));
}
// ...
}
return redirect()->to(adminUrl('indexing_api?slug=' . cleanStr($slug)))->withInput();
}
And in the model, return the response as an array:
IndexingModel
public function AddUrlToGoogle($google_url, $Urltype) {
// ...
$response = $httpClient->post($endpoint,['body' => $content]);
return json_decode($response->getBody() ?? '', true); // return an array
}
I know that for experienced Laravel developers this question my sound silly, but I followed this article for implementing Facebook SDK.
I followed everything from adding new token column in database to implementing controller.
This is my GraphController.php file:
class GraphController extends Controller
{
private $api;
public function __construct(Facebook $fb)
{
$this->middleware(function ($request, $next) use ($fb) {
$fb->setDefaultAccessToken(Auth::user()->token);
$this->api = $fb;
return $next($request);
});
}
public function getPageAccessToken($page_id){
try {
// Get the \Facebook\GraphNodes\GraphUser object for the current user.
// If you provided a 'default_access_token', the '{access-token}' is optional.
$response = $this->api->get('/me/accounts', Auth::user()->token);
} catch(FacebookResponseException $e) {
// When Graph returns an error
echo 'Graph returned an error: ' . $e->getMessage();
exit;
} catch(FacebookSDKException $e) {
// When validation fails or other local issues
echo 'Facebook SDK returned an error: ' . $e->getMessage();
exit;
}
try {
$pages = $response->getGraphEdge()->asArray();
foreach ($pages as $key) {
if ($key['id'] == $page_id) {
return $key['access_token'];
}
}
} catch (FacebookSDKException $e) {
dd($e); // handle exception
}
}
public function publishToPage(Request $request, $title){
$page_id = 'XXXXXXXXXXXXX';
try {
$post = $this->api->post('/' . $page_id . '/feed', array('message' => $title), $this->getPageAccessToken($page_id));
$post = $post->getGraphNode()->asArray();
} catch (FacebookSDKException $e) {
dd($e); // handle exception
}
}
}
This is my routes/web.php :
Route::group(['middleware' => [
'auth'
]], function(){
Route::post('/page', 'GraphController#publishToPage');
});
FacebookServiceProvider:
class FacebookServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* #return void
*/
public function register()
{
$this->app->singleton(Facebook::class, function ($app) {
$config = config('services.facebook');
return new Facebook([
'app_id' => $config['client_id'],
'app_secret' => $config['client_secret'],
'default_graph_version' => 'v2.6',
]);
});
}
}
Now, I would need to use publishToPage inside of my PostController.php file:
public function store(Requests\PostRequest $request)
{
$data = $this->handleRequest($request);
$newPost = $request->user()->posts()->create($data);
$newPost->createTags($data["post_tags"]);
/*
// My other notifications that are working:
// OneSignal
OneSignal::sendNotificationToAll(
"New warning ".$newPost->title
);
// MailChimp
$this->notify($request, $newPost);
// Twitter
$newPost->notify(new ArticlePublished());
*/
// I WOULD NEED SOMETHING IN THIS WAY ALSO FOR FACEBOOK BUT THIS OBVIOUSLY DOESN'T WORK
GraphController::publishToPage($request, $newPost->title);
}
Can you please suggest good way how to do it from here?
I need to apologize again if this seems to you like basics of Laravel that I should know, but I really struggling to wrap my head around this and your suggestions would really help me to understand it better.
Integrating Twitter, MailChimp, OneSignal notifications was really easy but Facebook restricted policies makes it quite confusing for me.
Thank you guys. I really appreciate it!
Sadly, Facebook still didn't get me permission for auto posting so I cannot try, if it realy works.
I think I found a solution to this particular problem though. Credit goes to Sti3bas from Laracast.
namespace App\Services;
class FacebookPoster
{
protected $api;
public function __construct(Facebook $fb)
{
$fb->setDefaultAccessToken(Auth::user()->token);
$this->api = $fb;
}
protected function getPageAccessToken($page_id){
try {
// Get the \Facebook\GraphNodes\GraphUser object for the current user.
// If you provided a 'default_access_token', the '{access-token}' is optional.
$response = $this->api->get('/me/accounts', Auth::user()->token);
} catch(FacebookResponseException $e) {
// When Graph returns an error
echo 'Graph returned an error: ' . $e->getMessage();
exit;
} catch(FacebookSDKException $e) {
// When validation fails or other local issues
echo 'Facebook SDK returned an error: ' . $e->getMessage();
exit;
}
try {
$pages = $response->getGraphEdge()->asArray();
foreach ($pages as $key) {
if ($key['id'] == $page_id) {
return $key['access_token'];
}
}
} catch (FacebookSDKException $e) {
dd($e); // handle exception
}
}
public function publishToPage($page, $title){
try {
$post = $this->api->post('/' . $page . '/feed', array('message' => $title), $this->getPageAccessToken($page));
$post = $post->getGraphNode()->asArray();
} catch (FacebookSDKException $e) {
dd($e); // handle exception
}
}
}
Then refact controllers:
use App\Services\FacebookPoster;
//...
class GraphController extends Controller
{
public function publishToPage(Request $request, FacebookPoster $facebookPoster)
{
$page_id = 'XXXXXXXXXXXXX';
$title = 'XXXXXXXXXXXXX';
$facebookPoster->publishToPage($page_id, $title);
}
}
use App\Services\FacebookPoster;
//...
public function store(PostRequest $request, FacebookPoster $facebookPoster)
{
$data = $this->handleRequest($request);
$newPost = $request->user()->posts()->create($data);
$newPost->createTags($data["post_tags"]);
//...
$facebookPoster->publishToPage($page, $newPost->title);
}
I'm using dingo/api (that has built-in support for jwt-auth) to make an API.
Suppose this is my routes :
$api->group(['prefix' => 'auth', 'namespace' => 'Auth'], function ($api) {
$api->post('checkPhone', 'LoginController#checkPhone');
//Protected Endpoints
$api->group(['middleware' => 'api.auth'], function ($api) {
$api->post('sendCode', 'LoginController#sendCode');
$api->post('verifyCode', 'LoginController#verifyCode');
});
});
checkPhone method that has task of authorize and creating token is like :
public function checkPhone (Request $request)
{
$phone_number = $request->get('phone_number');
if (User::where('phone_number', $phone_number)->exists()) {
$user = User::where('phone_number', $phone_number)->first();
$user->injectToken();
return $this->response->item($user, new UserTransformer);
} else {
return $this->response->error('Not Found Phone Number', 404);
}
}
And injectToken() method on User Model is :
public function injectToken ()
{
$this->token = JWTAuth::fromUser($this);
return $this;
}
Token creation works fine.
But When I send it to a protected Endpoint, always Unable to authenticate with invalid token occures.
The protected Endpoint action method is :
public function verifyCode (Request $request)
{
$phone_number = $request->get('phone_number');
$user_code = $request->get('user_code');
$user = User::wherePhoneNumber($phone_number)->first();
if ($user) {
$lastCode = $user->codes()->latest()->first();
if (Carbon::now() > $lastCode->expire_time) {
return $this->response->error('Code Is Expired', 500);
} else {
$code = $lastCode->code;
if ($user_code == $code) {
$user->update(['status' => true]);
return ['success' => true];
} else {
return $this->response->error('Wrong Code', 500);
}
}
} else {
return $this->response->error('User Not Found', 404);
}
}
I used PostMan as API client and send generated tokens as a header like this :
Authorization:Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI5ODkxMzk2MTYyNDYiLCJpc3MiOiJodHRwOlwvXC9hcGkucGFycy1hcHAuZGV2XC92MVwvYXV0aFwvY2hlY2tQaG9uZSIsImlhdCI6MTQ3NzEyMTI0MCwiZXhwIjoxNDc3MTI0ODQwLCJuYmYiOjE0NzcxMjEyNDAsImp0aSI6IjNiMjJlMjUxMTk4NzZmMzdjYWE5OThhM2JiZWI2YWM2In0.EEj32BoH0URg2Drwc22_CU8ll--puQT3Q1NNHC0LWW4
I Can not find solution after many search on the web and related repositories.
What is Problem in your opinion?
Update :
I found that not found error is for constructor of loginController that laravel offers :
public function __construct ()
{
$this->middleware('guest', ['except' => 'logout']);
}
because when I commented $this->middleware('guest', ['except' => 'logout']); all things worked.
But if I remove this line is correct?
How should be this line for APIs?
updating my config/api.php to this did the trick
// config/api.php
...
'auth' => [
'jwt' => 'Dingo\Api\Auth\Provider\JWT'
],
...
As I mentioned earlier as an Update note problem was that I used checkPhone and verifyCode in LoginController that has a check for guest in it's constructor.
And because guest middleware refers to \App\Http\Middleware\RedirectIfAuthenticated::class and that redirects logged in user to a /home directory and I did not created that, so 404 error occured.
Now just I moved those methods to a UserController without any middleware in it's constructor.
Always worth reading through the source to see whats happening. Answer: The is expecting the identifier of the auth provider in order to retrieve the user.
/**
* Authenticate request with a JWT.
*
* #param \Illuminate\Http\Request $request
* #param \Dingo\Api\Routing\Route $route
*
* #return mixed
*/
public function authenticate(Request $request, Route $route)
{
$token = $this->getToken($request);
try {
if (! $user = $this->auth->setToken($token)->authenticate()) {
throw new UnauthorizedHttpException('JWTAuth', 'Unable to authenticate with invalid token.');
}
} catch (JWTException $exception) {
throw new UnauthorizedHttpException('JWTAuth', $exception->getMessage(), $exception);
}
return $user;
}
Hey guys i got some Problems with the Slim Middleware.
I created a Middleware that checks if the user is logged with Facebook and has a specific Email address. So now when i call the url with the PHPStorm RESTful Test tool i should not be able to post data to the server...
But the Redirect does not work so i will be able to send data to the server.
/**
* Admin Middleware
*
* Executed before /admin/ route
*/
$adminPageMiddleware = function ($request, $response, $next) {
FBLoginCtrl::getInstance();
$user = isset($_SESSION['user']) ? $_SESSION['user'] : new User();
if (!($user->getEmail() == ADMIN_USER_EMAIL)) {
$response = $response->withRedirect($this->router->pathFor('login'), 403);
}
$response = $next($request, $response);
return $response;
};
/**
* Milestone POST Method
*
* Create new Milestone
*/
$app->post('/admin/milestone', function (Request $request, Response $response) use ($app) {
$milestones = $request->getParsedBody();
$milestones = isset($milestones[0]) ? $milestones : array($milestones);
foreach ($milestones as $milestone) {
$ms = new Milestone();
$msRepo = new MilestoneRepository($ms);
$msRepo->setJsonData($milestone);
if (!$msRepo->createMilestone()) {
return $response->getBody()->write("Not Okay");
};
}
return $response->getBody()->write("Okay");
})->add($adminPageMiddleware);
So can anyone give me a hint what the problem could be?
I tried to add the same middleware to the get Route ... there it works :/ Strange stuff.
The problem is in your middleware logic.
if (!($user->getEmail() == ADMIN_USER_EMAIL)) {
return $response->withRedirect($this->router->pathFor('login'), 403); //We do not want to continue execution
}
$response = $next($request, $response);
return $response;
So now i ended up with this code:
class AdminRouteMiddleware
{
public function __invoke($request, $response, $next)
{
FBLoginCtrl::getInstance();
$user = isset($_SESSION['user']) ? $_SESSION['user'] : new User();
if (!($user->getEmail() == ADMIN_USER_EMAIL)) {
if ($_SERVER['REQUEST_METHOD'] == "GET") {
$response = $response->withRedirect('/login', 403);//want to use the route name instead of the url
} else {
$response->getBody()->write('{"error":Access Denied"}');
}
} else {
$response = $next($request, $response);
}
return $response;
}
}
/**
* Milestone POST Method
*
* Create new Milestone
*/
$app->post('/admin/milestone', function (Request $request, Response $response) use ($app) {
$milestones = $request->getParsedBody();
$milestones = isset($milestones[0]) ? $milestones : array($milestones);
foreach ($milestones as $milestone) {
$ms = new Milestone();
$msRepo = new MilestoneRepository($ms);
$msRepo->setJsonData($milestone);
if (!$msRepo->createMilestone()) {
return $response->getBody()->write("Not Okay");
};
}
return $response->getBody()->write("Okay");
})->add(new AdminRouteMiddleware());
Is there an efficient way to do this? Options I've looked into:
Checking the session container in the layout
Checking the session container in the module onBootstrap functions()
Handling the session container individually in each Controller/Action
Ideally I'd have this check once, is there any correct way to do this?
Something along the lines of...
$session = new Container('username');
if($session->offsetExists('username')) {
//check im not already at my login route
//else redirect to login route
}
}
You can use below code inside each controller
public function onDispatch(\Zend\Mvc\MvcEvent $e)
{
if (! $this->authservice->hasIdentity()) {
return $this->redirect()->toRoute('login');
}
return parent::onDispatch($e);
}
You can also check session on module's onBootstrap function(), you need to match the route using zf2 events:
$auth = $sm->get('AuthService');
$em->attach(MvcEvent::EVENT_ROUTE, function ($e) use($list, $auth)
{
$match = $e->getRouteMatch();
// No route match, this is a 404
if (! $match instanceof RouteMatch) {
return;
}
// Route is whitelisted
$name = $match->getMatchedRouteName();
if (in_array($name, $list)) {
return;
}
// User is authenticated
if ($auth->hasIdentity()) {
return;
}
// Redirect to the user login page, as an example
$router = $e->getRouter();
$url = $router->assemble(array(), array(
'name' => 'login'
));
$response = $e->getResponse();
$response->getHeaders()
->addHeaderLine('Location', $url);
$response->setStatusCode(302);
return $response;
}, - 100);
where $list will contain the list of routes not to be processed:
$list = array('login', 'login/authenticate');
As checkout in ZFcAuth plugin in following urls, I found some code for check & redirect.
if (!$auth->hasIdentity() && $routeMatch->getMatchedRouteName() != 'user/login') {
$response = $e->getResponse();
$response->getHeaders()->addHeaderLine(
'Location',
$e->getRouter()->assemble(
array(),
array('name' => 'zfcuser/login')
)
);
$response->setStatusCode(302);
return $response;
}
This code chunk show the way to validate/redirect. However they are not in-build way as ZF2 only provide components. You can also use other plugins like ZfcUser, ZfcAcl, ZfcRABC which provide all functionality.
link : https://github.com/ZF-Commons/ZfcUser/issues/187#issuecomment-12088823 .