Need you help to figure this out. i developed my website in Slim 3 framework. I wanted to handle "Method not allowed. Must be one of: POST" message which i get when i am using browser back and forward buttons.
I want to redirect to a different page when if the route is post and when user clicks on browser back or forward page.
When the post route is called is there a way where i can detect the that it is post method call and redirect him to a different get route.
You can add your own handler for specific errors:
$container['notAllowedHandler'] = function (ServerRequestInterface $request, ResponseInterface $response, array $methods) {
// you can return a redirect response
};
see more here
Another syntax solution
$notAllowedHandler = function ($c) {
return function ($request, $response) use ($c) {
return $response
->withJson(
[
"status" => false,
"message" => "Your custom message",
"data" => [],
]
)
->withStatus(400);
};
};
$app = new App(
[
'notAllowedHandler' => $notAllowedHandler,
]
);
Related
I have the following two settings page routes one for normal settings and the other for secured and admin related settings which uses the same middleware "password.confirm"
Route::get('/admin/settings',WebsiteInfoController::class,'edit'])->name('settings')->middleware('password.confirm');
Route::get('/settings',[WebsiteInfoController::class, edit'])->name('user.settings')->middleware('password.confirm');
This middleware redirects me to a second page where I have to enter password and then only i can get access to my intended page.
In my middleware I have the following function. I want to make an additional check if the user is intending to access the admin related settings
public function store(Request $request)
{
$this->validate($request, [
'secret_password' => ['sometimes','required'],
'password' => ['required','password:web'],
]);
if(redirect()->intended()->getTargetUrl()==route('settings')){
$secret_password =WebsiteInfo::first()->secret_password;
if (!Hash::check($request->secret_password, $secret_password)) {
throw ValidationException::withMessages([
'secret_password' => __('auth.password'),
]);
}
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended(RouteServiceProvider::HOME);
});
});
}
Everything works fine in this method but the intended URL is lost when all the check is performed and I am redirected to homepage instead of settings page. I also tried to save the URL in a variable and use it later in the redirect command like
$path=redirect()->intended()->getTargetUrl();
if($path==route('settings')){
$secret_password =WebsiteInfo::first()->secret_password;
if (!Hash::check($request->secret_password, $secret_password)) {
throw ValidationException::withMessages([
'secret_password' => __('auth.password'),
]);
}
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended($path);
This method works fine but it also loses the URL if the second validation fails and the user is redirected back to the confirm password page. Now when I try to perform the validation second time it again loses the intended URL and redirects me back to home page.
i also tried the check with $request method.
if($request->route()->named('settings')){
$secret_password =WebsiteInfo::first()->secret_password;
if (!Hash::check($request->secret_password, $secret_password)) {
throw ValidationException::withMessages([
'secret_password' => __('auth.password'),
]);
}
}
This method however, is not able to detect the route in middleware and the validation check is not at all performed.
So, My question is how do i check for the intended URL and perform validation check without losing the intended URL even after multiple failed validation attempts?
Your method is all fine. You just used the wrong method to extract the target website URL. It is true that redirect()->intended()->getTargetUrl() gives you the target page URL but it also removes the target website URL from the session so when you finish performing the checks and want to redirect to the intended page there is no intended page URL found in the session and you get redirected to the default fall back URL. This is what the redirect function does
public function intended($default = '/', $status = 302, $headers = [], $secure = null) {
$path = $this->session->pull('url.intended', $default);
return $this->to($path, $status, $headers, $secure);
}
Here, the $request->route()->named('settings) method does not work since you are not directly interacting with your initial view but instead through a middleware view which does not send the intended page request.
Use the following code and I guess you will be all fine with your validation attempts. It will work even after multiple failed login attempts.
public function store(Request $request) {
$this->validate($request, [
'secret_password' => ['sometimes','required'],
'password' => ['required','password:web'],
]);
$path=session()->get('url.intended', RouteServiceProvider::HOME);
if($path==route('settings')) {
$secret_password =WebsiteInfo::first()->secret_password;
if (!Hash::check($request->secret_password, $secret_password)) {
throw ValidationException::withMessages([
'secret_password' => __('auth.password'),
]);
}
}
$request->session()->put('auth.password_confirmed_at', time());
return redirect()->intended($path);
}
I'm strugling with authorization middleware in Slim4. Here's my code:
$app = AppFactory::create();
$app->add(new Authentication());
$app->group('/providers', function(RouteCollectorProxy $group){
$group->get('/', 'Project\Controller\ProviderController:get');
})->add(new SuperuserAuthorization());
Authentication middleware checks the user and works fine.
The method get in ProviderController is
public function get(Request $request, Response $response): Response{
$payload = [];
foreach(Provider::all() as $provider){
$payload[] = [
'id' => $provider->id,
'name' => $provider->name,
];
}
$response->getBody()->write(json_encode($payload));
return $response;
}
The SuperuserAuthorization looks like this
class SuperuserAuthorization{
public function __invoke(Request $request, RequestHandler $handler): Response{
$response = $handler->handle($request);
$authorization = explode(" ", $request->getHeader('Authorization')[0]);
$user = User::getUserByApiKey($authorization[1]);
if(! Role::isSuperuser($user)){
return $response->withStatus(403);//Forbidden
}
return $response;
}
}
The thing is that even though the user is not a superuser, the application continues executing. As a result I get json with all the providers and http code 403 :/
Shouldn't route middleware stop the request from getting into the app and just return 403 right away?
I know that I can create new empty response with status 403, so the data won't come out, but the point is that the request should never get beyond this middleware, am I right or did I just misunderstand something hereā¦
Any help will be appreciated :)
------------- SOLUTION ----------------
Thanks to #Nima I solved it. The updated version of middleware is:
class SuperuserAuthorization{
public function __invoke(Request $request, RequestHandler $handler): Response{
$authorization = explode(" ", $request->getHeader('Authorization')[0]);
$user = User::getUserByApiKey($authorization[1]);
if(! Role::isSuperuser($user)){
$response = new Response();
return $response->withStatus(403);//Forbidden
}
return $handler->handle($request);
}
}
Shouldn't route middleware stop the request from getting into the app and just return 403 right away?
Slim 4 uses PSR-15 compatible middlewares. There is good example of how to implement an authorization middleware in PSR-15 meta document. You need to avoid calling $handler->handle($request) if you don't want the request to be processed any further.
As you can see in the example, if the request is not authorized, a response different from the return value of $handler->handle($request) is returned. This means your point saying:
I know that I can create new empty response with status 403, so the data won't come out, but the point is that the request should never get beyond this middleware
is somehow correct, but you should prevent the request from going further by returning appropriate response before invoking the handler, or throwing an exception and let the error handler handle it.
Here is a simple middleware that randomly authorizes some of requests and throws an exception for others:
$app->group('/protected', function($group){
$group->get('/', function($request, $response){
$response->getBody()->write('Some protected response...');
return $response;
});
})->add(function($request, $handler){
// randomly authorize/reject requests
if(rand() % 2) {
// Instead of throwing an exception, you can return an appropriate response
throw new \Slim\Exception\HttpForbiddenException($request);
}
$response = $handler->handle($request);
$response->getBody()->write('(this request was authorized by the middleware)');
return $response;
});
To see different responses, please visit /protected/ path a few times (remember the middleware acts randomly)
I'm trying to make a simple route to redirect endpoints to a specific URL based on an array/key.
$redirects = [
"/ios" => $GLOBALS['config']['iosAppStoreLink'],
"/android" => $GLOBALS['config']['androidAppStoreLink']
];
/**
* Redirects
*/
foreach($redirects as $endpoint => $url) {
$app->get($endpoint, function($request, $response) {
return $response->withRedirect($url);
});
}
The endpoints get created without issue just once I'm inside of the $app->get function, it will not allow me to use $url... I get an Undefined Index error in my console.
What am I doing wrong here, why am I not able to access the $url variable?
To allow the function to access the $url from outside of it's own scope, you could use function() use() { syntax...
foreach($redirects as $endpoint => $url) {
$app->get($endpoint, function($request, $response) use ($url) {
return $response->withRedirect($url);
});
}
I'm trying to implement json-schema validator from justinrainbow as middleware in Slim 3.
I can't figure out how to get the clients input from GET/POST requests in middleware.
tried like this:
$mw = function ($request, $response, $next) {
$data = $request->getParsedBody();
print_r($data); // prints nothing
$id = $request->getAttribute('loan_id');
print_r($id); // prints nothing
// here I need to validate the user input from GET/POST requests with json-schema library and send the result to controller
$response = $next($request, $response);
return $response;
};
$app->get('/loan/{loan_id}', function (Request $request, Response $response) use ($app, $model) {
$loanId = $request->getAttribute('loan_id'); // here it works
$data = $model->getLoan($loanId);
$newResponse = $response->withJson($data, 201);
return $newResponse;
})->add($mw);
There are 2 possible ways of how I need it. what i'm doing wrong ?
validate it in middleware and send some array/json response to the controller, which i will then get as I understood with $data = $request->getParsedBody();
validate it in middleware but final check will be in controller like this:
$app->get('/loan/{loan_id}', function (Request $request, Response $response) use ($app, $model) {
if($validator->isValid()){
//
}
$loanId = $request->getAttribute('loan_id'); // here it works
$data = $model->getLoan($loanId);
$newResponse = $response->withJson($data, 201);
return $newResponse;
})->add($mw);
Best option for me it do something like here
but I don't understand what should i return in container, and how to pass get/post input to container
Your code in the first point seems alright, you only try to access route parameter from within middleware. At that point the route is not yet resolved and therefore parameters are not parsed from the URL.
This is a known use case and is described in Slim's documentation. Add the following setting to your app configuration to get your code working:
$app = new App([
'settings' => [
// Only set this if you need access to route within middleware
'determineRouteBeforeAppMiddleware' => true
]
]);
In order to understand how middleware works and how to manipulate response object, I suggest you read the User Guide - it's not that long and explains it really well.
Good day everyone!
I have a working slim code here with slim-basic-auth and when I go to a restricted directory, this shows up:
Everything works, but what I wanted to do is to redirect it to my login page instead of showing a popup login box. Here is my login page:
My slim code:
$pdo = new \PDO("mysql:host=localhost;dbname=databasename", "username");
$app->add(new \Slim\Middleware\HttpBasicAuthentication([
"path" => "/main",
"realm" => "Protected",
"authenticator" => new PdoAuthenticator([
"pdo" => $pdo,
"table" => "accounts",
"user" => "accountUsername",
"hash" => "accountPassword"
]),
"callback" => function ($request, $response, $arguments) use ($app) {
return $response->withRedirect('/main/contacts');
}
When I try to login using the popup login box, it works but I really want to redirect it to my login page instead of that.
Any help would be much appreciated.
The middleware implements HTTP Basic Access Authentication. Authentication dialog is triggered via response header. It is up to the browser vendor to decide how credentials are asked. Most browsers use the popup login dialog you described.
What you are trying to do is a bit unorthodox way of using HTTP Basic Authentication. However you can suppress the login dialog by removing the WWW-Authenticate header from the response. Note the you need at least version 2.0.2 for this to work.
$app->add(new \Slim\Middleware\HttpBasicAuthentication([
"path" => ["/main"],
"authenticator" => new PdoAuthenticator([
"pdo" => $pdo,
"table" => "accounts",
"user" => "accountUsername",
"hash" => "accountPassword"
]),
"error" => function ($request, $response, $arguments) {
return $response
->withRedirect("/auth/login")
->withoutHeader("WWW-Authenticate");
}
]));
However with code above you still have to set the Authentication: Basic request header somehow. One way to do is using an AJAX request.
$.ajax({
url: "http://example.com/auth/login",
username: $("username").val(),
password: $("password").val(),
success: function(result) {
alert("Authorization header should now be set...");
}
});
At this point it looks like you're not trying to use the Http Basic Authenticator but rather a normal login process so you'll need to use sessions and such.
A very simple example is adding this close to the bottom of your middleware stack.(meaning it will be executed first as it will be at the top of the stack)
$middleware = function (Request $request, Response $response, $next) {
if (!isset($_SESSION['__user'])) {
//don't interfere with unmatched routes
$route = $request->getAttribute('route');
if ($route && !in_array($route->getName(), ['login'])) {
return $response->withStatus(403)->withHeader('Location', $this->router->pathFor('login'));
}
}
return $next($request, $response);
};
$app->add($middleware);
Looking at the HttpBasicAuthentication middleware it will always send the WWW-Authenticate header making your login form useless as it will trigger the auth pop-up.