Secure method using annotations - php

I have a page with a form and want to know if it is possible to access it using GET, but only allow logged in users to POST to it.
I know this can be done in security.yml, but am not sure how to do it with annotations.
/**
* #param Request $request
* #return Response
* #Security("has_role('ROLE_USER')")
* #Method(methods={"POST"})
*/
public function calculatorAction(Request $request)
{
$form=$this->createForm(new CallRequestType(),$callReq=new CallRequest());
$form->handleRequest($request);
if($form->isValid()){
//blabla
}
return $this->render('MyBundle:Pages:calculator.html.twig', array('form' => $form));
}
This will secure the whole function, but I want to access it, just not POST to it without being logged in. An alternative would be to check if there is a logged in user in the $form->isValid() bracket. But I'm still wondering if it can be done with annotations.

You could do something like this.
You can allow both method types anonymously, and check just inside the controller to see if the user is authenticated and is POSTing.
(You don't state which version of symfony you're using, so you might have to substitute the authorization_checker (2.8) for the older security.context service)
/**
* #param Request $request
* #return Response
*
* #Route("/someroute", name="something")
* #Method(methods={"POST", "GET"})
*/
public function calculatorAction(Request $request)
{
if ( !$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY') && $request->getMethod() == 'POST') {
throw new AccessDeniedHttpException();
}
$form=$this->createForm(new CallRequestType(),$callReq=new CallRequest());
$form->handleRequest($request);
// you also need to check submitted or youll fire the validation on every run through.
if($form->isSubmitted() && $form->isValid()){
//blabla
}
return $this->render('MyBundle:Pages:calculator.html.twig', array('form' => $form));
}

Related

How to check (in template) if $user->can('access', $request) in CakePHP 4?

I've created a RequestPolicy in src/Policy/RequestPolicy.php to allow access to all actions of my SuperRubriquesController only to a "super-admin" user :
namespace App\Policy;
use Authorization\Policy\RequestPolicyInterface;
use Cake\Http\ServerRequest;
use Authorization\IdentityInterface;
class RequestPolicy implements RequestPolicyInterface
{
/**
* Method to check if the request can be accessed
*
* #param \Authorization\IdentityInterface|null $identity Identity
* #param \Cake\Http\ServerRequest $request Server Request
* #return bool
*/
public function canAccess($identity, ServerRequest $request)
{
if ($request->getParam('controller') === 'SuperRubriques' && $identity) {
return $identity->role === 'super-admin';
}
return true;
}
}
It works fine when I go to "/super-rubriques/index" or others actions of SuperRubriquesController but I'm wondering if there's a way to check if a user can access to a request from a template.
For example, I'd like to check if user can access to action index of SuperRubriquesController before to display the link.
if ($this->request->getAttribute('identity')->can('access', $requestToSuperRubriquesIndex)) {
echo $this->Html->link('Super Rubriques', ['controller' => 'SuperRubriques', 'action' => 'index']);
}
How can I build $requestToSuperRubriquesIndex ?
One way would be to use the with* methods of the current request object to create a clone with modified data:
$requestToSuperRubriquesIndex = $this->request
->withParam('controller', 'SuperRubriques')
->withParam('action', 'index');
See also
API > \Cake\Http\ServerRequest::withParam()

How to extend or make custom PasswordBroker sendResetLink() method in Laravel 5.8?

Currently the logic behind Resetting Password is that user must provide valid/registered e-mail to receive password recovery e-mail.
In my case I don't want to validate if the e-mail is registered or not due to security concerns and I want to just do the check in back-end and tell user that "If he has provided registered e-mail, he should get recovery e-mail shortly".
What I've done to achieve this is edited in vendor\laravel\framework\src\Illuminate\Auth\Passwords\PasswordBroker.php sendResetLink() method from this:
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
if (is_null($user)) {
return static::INVALID_USER;
}
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
return static::RESET_LINK_SENT;
}
to this:
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
// if (is_null($user)) {
// return static::INVALID_USER;
// }
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
if(!is_null($user)) {
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
}
return static::RESET_LINK_SENT;
}
This hard-coded option is not the best solution because it will disappear after update.. so I would like to know how can I extend or implement this change within the project scope within App folder to preserve this change at all times?
P.S. I've tried solution mentioned here: Laravel 5.3 Password Broker Customization but it didn't work.. also directory tree differs and I couldn't understand where to put new PasswordBroker.php file.
Thanks in advance!
Here are the steps you need to follow.
Create a new custom PasswordResetsServiceProvider. I have a folder (namespace) called Extensions where I'll place this file:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordResetServiceProvider as BasePasswordResetServiceProvider;
class PasswordResetServiceProvider extends BasePasswordResetServiceProvider
{
/**
* Indicates if loading of the provider is deferred.
*
* #var bool
*/
protected $defer = true;
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
$this->registerPasswordBroker();
}
/**
* Register the password broker instance.
*
* #return void
*/
protected function registerPasswordBroker()
{
$this->app->singleton('auth.password', function ($app) {
return new PasswordBrokerManager($app);
});
$this->app->bind('auth.password.broker', function ($app) {
return $app->make('auth.password')->broker();
});
}
}
As you can see this provider extends the base password reset provider. The only thing that changes is that we are returning a custom PasswordBrokerManager from the registerPasswordBroker method. Let's create a custom Broker manager in the same namespace:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordBrokerManager as BasePasswordBrokerManager;
class PasswordBrokerManager extends BasePasswordBrokerManager
{
/**
* Resolve the given broker.
*
* #param string $name
* #return \Illuminate\Contracts\Auth\PasswordBroker
*
* #throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException(
"Password resetter [{$name}] is not defined."
);
}
// The password broker uses a token repository to validate tokens and send user
// password e-mails, as well as validating that password reset process as an
// aggregate service of sorts providing a convenient interface for resets.
return new PasswordBroker(
$this->createTokenRepository($config),
$this->app['auth']->createUserProvider($config['provider'] ?? null)
);
}
}
Again, this PasswordBrokerManager extends the base manager from laravel. The only difference here is the new resolve method which returns a new and custom PasswordBroker from the same namespace. So the last file we'll create a custom PasswordBroker in the same namespace:
<?php
namespace App\Extensions\Passwords;
use Illuminate\Auth\Passwords\PasswordBroker as BasePasswordBroker;
class PasswordBroker extends BasePasswordBroker
{
/**
* Send a password reset link to a user.
*
* #param array $credentials
* #return string
*/
public function sendResetLink(array $credentials)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
// "flash" data in the session to indicate to the developers the errors.
$user = $this->getUser($credentials);
// if (is_null($user)) {
// return static::INVALID_USER;
// }
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
if(!is_null($user)) {
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
}
return static::RESET_LINK_SENT;
}
}
As you can see we extend the default PasswordBroker class from Laravel and only override the method we need to override.
The final step is to simply replace the Laravel Default PasswordReset broker with ours. In the config/app.php file, change the line that registers the provider as such:
'providers' => [
...
// Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
App\Extensions\Passwords\PasswordResetServiceProvider::class,
...
]
That's all you need to register a custom password broker. Hope that helps.
The easiest solution here would be to place your customised code in app\Http\Controllers\Auth\ForgotPasswordController - this is the controller that pulls in the SendsPasswordResetEmails trait.
Your method overrides the one provided by that trait, so it will be called instead of the one in the trait. You could override the whole sendResetLinkEmail method with your code to always return the same response regardless of success.
public function sendResetLinkEmail(Request $request)
{
$this->validateEmail($request);
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$response = $this->broker()->sendResetLink(
$request->only('email')
);
return back()->with('status', "If you've provided registered e-mail, you should get recovery e-mail shortly.");
}
You can just override the sendResetLinkFailedResponse method in your ForgetPasswordController class.
protected function sendResetLinkFailedResponse(Request $request, $response)
{
return $this->sendResetLinkResponse($request, Password::RESET_LINK_SENT);
}
We'll just send the successful response even if the validation failed.

Call API Platform default operation from a custom one?

I'm working on security zone. After spending days on Symfony and API Platform docs, i realised that my concern is particula.
Talking about security in Symfony, I think it's cool for small or internal projects. We can add/remove profiles/roles like we want.
I want to do something special. I want user or client (who is not dev) to be able via the back office, add new profiles and assign rights. I use security zone instead of roles directly. I want to protect my actions(controller) under security zone.
Each controller gets "getSecurityZone()" method which can allow me to check that first (onkernel event) to see if the current user has rights (depending on their -unique- role/profile) on the ressource.
public function securityZone(): string
{
return 'BO_ZONE';
}
My concern: I don't want to rewrite the logic to retrieve data since API Platform already does it. So i tried to use event to run my security check. It seems working but How to call default operation (like GET) from my action ?
My entity :
* },
* collectionOperations={
* "get"={
* "method"="GET",
* "path"="/users",
* "controller"=UserListAction::class,
* "defaults"={"_api_receive"=false},
* "swagger_context"={
* "parameters"={
*
* }
* },
* },
* "post"
* }
* )
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #ORM\Table(name="users")
*/
class User implements UserInterface
{
My eventlistener :
public static function getSubscribedEvents()
{
return [
KernelEvents::VIEW => ['encodePassword', EventPriorities::PRE_WRITE],
KernelEvents::CONTROLLER => ['micheckSecurity', EventPriorities::PRE_READ],
];
}
//..
public function micheckSecurity(FilterControllerEvent $event)
{
$controller = $event->getController();
$method = $event->getRequest()->getMethod();
if (Request::METHOD_GET !== $method || $controller->getSecurityZone() !== Constants::SECZONE_BO_ZONE)
throw new AccessDeniedException("You are not author", 500);
return;
}
And my action(controller) :
class UserListAction
{
public function __invoke()
{
//Call default operation which return collection
}
public function securityZone(): string
{
return 'BO_ZONE';
}
}
In my _invoke() method, I want to call the read method of API Platform which will return the collection automatically.
Thank you for your attention.

Refreshing authentication tokens for a Vue.js SPA using Laravel for the backend

I am building a single-page-app with Vue (2.5) using Laravel (5.5) as the backend. Everything works well, except for directly logging in again after having logged out. In this case, the call to /api/user (to retrieve the user's account information and to verify the user's identity once more) fails with a 401 unauthorized (even though the log-in succeeded). As a response, the user is bounced back directly to the login screen (I wrote this measure myself as a reaction to 401 responses).
What does work is to log out, refresh the page with ctrl/cmd+R, and then log in again. The fact that a page refresh fixes my problem, gives me reason to believe that I am not handling refresh of the X-CSRF-TOKEN correctly, or may be forgetting about certain cookies that Laravel uses (as described here ).
This is a snippet of the code of the login form that is executed after a user clicks the login button.
login(){
// Copy the form data
const data = {...this.user};
// If remember is false, don't send the parameter to the server
if(data.remember === false){
delete data.remember;
}
this.authenticating = true;
this.authenticate(data)
.then( this.refreshTokens )
.catch( error => {
this.authenticating = false;
if(error.response && [422, 423].includes(error.response.status) ){
this.validationErrors = error.response.data.errors;
this.showErrorMessage(error.response.data.message);
}else{
this.showErrorMessage(error.message);
}
});
},
refreshTokens(){
return new Promise((resolve, reject) => {
axios.get('/refreshtokens')
.then( response => {
window.Laravel.csrfToken = response.data.csrfToken;
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
this.authenticating = false;
this.$router.replace(this.$route.query.redirect || '/');
return resolve(response);
})
.catch( error => {
this.showErrorMessage(error.message);
reject(error);
});
});
},
the authenticate() method is a vuex action, which calls the login endpoint at the laravel side.
The /refreshTokens endpoint simply calls this Laravel controller function that returns the CSRF token of the currently logged-in user:
public function getCsrfToken(){
return ['csrfToken' => csrf_token()];
}
After the tokens have been refetched, the user is redirected to the main page (or another page if supplied)
with this.$router.replace(this.$route.query.redirect || '/'); and there the api/user function is called to check the data of the currently logged in user.
Are there any other measures I should take to make this work, that I am overlooking?
Thanks for any help!
EDIT: 07 Nov 2017
After all the helpful suggestions, I would like to add some information. I am using Passport to authenticate on the Laravel side, and the CreateFreshApiToken middleware is in place.
I have been looking at the cookies set by my app, and in particular the laravel_token which is said to hold the encrypted JWT that Passport will use to authenticate API requests from your JavaScript application. When logging out, the laravel_token cookie is deleted. When logging in again directly afterwards (using axios to send an AJAX post request) no new laravel_token is being set, so that's why it doesn't authenticate the user. I am aware that Laravel doesn't set the cookie on the login POST request, but the GET request to /refreshTokens (which is not guarded) directly afterwards should set the cookie. However, this doesn't appear to be happening.
I have tried increasing the delay between the request to /refreshTokens and the request to /api/user, to maybe give the server some time to get things in order, but to no avail.
For completeness sake, here is my Auth\LoginController that is handling the login request server-side:
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
// $this->middleware('guest')->except('logout');
}
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(\Illuminate\Http\Request $request)
{
//return $request->only($this->username(), 'password');
return ['email' => $request->{$this->username()}, 'password' => $request->password, 'active' => 1];
}
/**
* The user has been authenticated.
*
* #param \Illuminate\Http\Request $request
* #param mixed $user
* #return mixed
*/
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
$user->last_login = \Carbon\Carbon::now();
$user->timestamps = false;
$user->save();
$user->timestamps = true;
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
);
}
/**
* Log the user out of the application.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function logout(\Illuminate\Http\Request $request)
{
$this->guard()->logout();
$request->session()->invalidate();
}
}
Considering that you are using an api for authentication, I would suggest using Passport or JWT Authentication to handle authentication tokens.
Finally fixed it!
By returning the UserResource directly in the LoginControllers authenticated method, it is not a valid Laravel Response (but I guess raw JSON data?) so probably things like cookies are not attached. I had to attach a call to response() on the resource and now everything seems to work fine (though I need to do more extensive testing).
So:
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
...
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
);
}
becomes
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
...
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
)->response(); // Add response to Resource
}
Hurray for the Laravel docs on attributing a section to this:
https://laravel.com/docs/5.5/eloquent-resources#resource-responses
Additionally, the laravel_token is not set by the POST request to login, and the call to refreshCsrfToken() also didn't do the trick, probably because it was protected by the guest middleware.
What worked for me in the end is to perform a dummy call to '/' right after the login function returned (or the promise was fulfilled).
In the end, my login function in the component was as follows:
login(){
// Copy the user object
const data = {...this.user};
// If remember is false, don't send the parameter to the server
if(data.remember === false){
delete data.remember;
}
this.authenticating = true;
this.authenticate(data)
.then( csrf_token => {
window.Laravel.csrfToken = csrf_token;
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrf_token;
// Perform a dummy GET request to the site root to obtain the larevel_token cookie
// which is used for authentication. Strangely enough this cookie is not set with the
// POST request to the login function.
axios.get('/')
.then( () => {
this.authenticating = false;
this.$router.replace(this.$route.query.redirect || '/');
})
.catch(e => this.showErrorMessage(e.message));
})
.catch( error => {
this.authenticating = false;
if(error.response && [422, 423].includes(error.response.status) ){
this.validationErrors = error.response.data.errors;
this.showErrorMessage(error.response.data.message);
}else{
this.showErrorMessage(error.message);
}
});
and the authenticate() action in my vuex store is as follows:
authenticate({ dispatch }, data){
return new Promise( (resolve, reject) => {
axios.post(LOGIN, data)
.then( response => {
const {csrf_token, ...user} = response.data;
// Set Vuex state
dispatch('setUser', user );
// Store the user data in local storage
Vue.ls.set('user', user );
return resolve(csrf_token);
})
.catch( error => reject(error) );
});
},
Because I didn't want to make an extra call to refreshTokens in addition to the dummy call to /, I attached the csrf_token to the response of the /login route of the backend:
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
$user->last_login = \Carbon\Carbon::now();
$user->timestamps = false;
$user->save();
$user->timestamps = true;
return (new UserResource($user))->additional([
'permissions' => $user->getUIPermissions(),
'csrf_token' => csrf_token()
])->response();
}
You should use Passports CreateFreshApiToken middleware in your web middleware passport consuming-your-api
web => [...,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
this attaches attach the right csrftoken() to all your Request headers as request_cookies

How to handle request from $_POST where no Symfony form is defined on the request, without searching form classname?

How can I handle request data from the $_POST data itself. I mean if I try to handle the form like this: $form->handleRequest($request);, Symfony would try to get the data from $_POST['form_classname'], but I want to fill my form class straight from base $_POST variables.
Actually I want to handle the information from the outer site. And I have to develop something like an API. But without authorization, tokens, etc...
So I decided to build the form with some properties I need. After validation the form might do some logic.
Here is an example of $_POST I have to handle
Function=TransResponse&RESULT=0&RC=00&AUTHCODE=745113
As you can see, there is no form name in request. The $form->handleRequest($request); works only if the request was like an
[form_name][Function]=TransResponse&[form_name][RESULT]=0&[form_name][RC]=00&[form_name][AUTHCODE]=745113
But I can't change the request format.
Just put in your form class
/** #inheritdoc */
function getBlockPrefix() {
return '';
}
Here is the information about this method Documentation
Use
$this->get('form.factory')->createNamed('')
$this->get('form.factory.)->createNamedBuilder('')
to create a Form or FormBuilder respectively that uses the whole $_POST/$_GET array for its parameters.
Example:
/**
* #Route("/testRoute")
* #param Request $request
* #return Response
*/
public function testAction(Request $request): Response
{
$form = $this->get('form.factory')->createNamedBuilder('', FormType::class, null, ['csrf_protection' => false])
->add('text', TextType::class)
->setMethod('GET')
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return new Response($form['text']->getData());
}
return new Response('Submit me');
}

Categories