Symfony: make CSRF token available to all twig templates - symfony

My app has AJAX requests here and there, and I want to protect them with CSRF tokens. However, rather than generating and passing a CSRF token to the Twig renderer for use in the JavaScript, I'd like a CSRF token to be readily available in every html page that Twig renders.
I've seen that Laravel seems to put it in a meta tag, so I can easily grab it with JavaScript. But how to do this in Symfony? How can I insert this token in every page?
Or is this not good practice?

Of course this is a good practice!!
You have the built-in function csrf_token('key') in symfony/twig for that.
Example:
<a href="{{ path('product_remove', {id: product.id, csrf: csrf_token('product') }) }}"
class="btn btn-info">Remove</a>
From a controller, you just have to check it:
/**
* #Route("/product/remove/{csrf}/{id}", name="product_remove")
*/
public function removeProductAction($csrf, $id)
{
if ($csrf !== $this->get('security.csrf.token_manager')->getToken('product')->getValue()) {
throw new InvalidCsrfTokenException('Invalid CSRF token');
}
// delete $id
// cleans up url
return $this->redirectToRoute('product_list');
}

The answer: insert it as a twig global variable and then use Javascript to bind it to all requests.
config.yml
twig:
## ...
globals:
csrfTokenManager: '#security.csrf.token_manager'
base.html.twig
<script>
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('x-csrf-token', '{{ csrfTokenManager.getToken('ajaxAuth') }}');
});
</script>
Receiving controller
public function receiveAction (Request $request)
{
$tokenManager = $this->get('security.csrf.token_manager');
$tokenId = 'ajaxAuth';
$token = new CsrfToken($tokenId, $request->headers->get('x-csrf-token'));
if (!$tokenManager->isTokenValid($token)) {
throw new HttpException(403, 'Go away h4x0r');
}
}

Related

Symfony : redirect after submit in Twig render() method

I have a few posts in Twig view. For each post, I want to add a form comment.
For best reusable, I call render(controller()) in Twig.
Twig :
{{ render(controller('App\\Controller\\User\\PostController::comment',
{
'post': post,
}
)) }}
Controller :
public function comment(Request $request, Post $post): Response
{
$comment = new PostComment();
$comment->setPost($post);
$form = $this->get('form.factory')->createNamedBuilder('post_comment_' . $post->getId(), PostCommentType::class, $comment)->getForm();
$form->handleRequest($this->get('request_stack')->getMasterRequest());
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($comment);
$entityManager->flush();
$this->redirectToRoute(...); // ERROR
}
return $this->render('user/post/_comment.html.twig', [
'form' => $form->createView(),
]);
}
But I can't redirect after submit. I understand that it's complicated from a view, HttpRequest has already passed.
This is error :
An exception has been thrown during the rendering of a template ("Error when rendering "http://project.local/index.php/u/root/post" (Status code is 302).").
Do you have a solution for me ? Thanks you :)
The main problem is handling your invalid form.
1) redirection during submission
If you don't care your user is on the comment page (not the render) if his form is invalid, you can simply add in your rendered template:
{# 'user/post/_comment.html.twig' #}
{{ form_start(form, {'action': path('your_comment_route')}) }}
or
2) Redirecting using javascript after submission
If you want the user to stay in the same page if the form has error, I don't see any other solution than adding a parameter to change the result of your controller
{{ render(controller('App\\Controller\\User\\PostController::comment', {
'post': post,
'embedded': true,
} )) }}
Then in your controller
if ($form->isSubmitted() && $form->isValid()) {
if($embedded) {
return $this->render('user/post/success.html.twig', [
//call this route by javascript?
'redirectRoute': $this->generateUrl()//call it in twig, it's just for better understanding
])
} else {
$this->redirectToRoute(...);
}
}

Securing HTML in AngularJS + ASP.NET

I've been searching about how to implement authentication/authorization in SPA's with AngularJS and ASP.NET Web API and I have one doubt. First we can implement authentication and authorization on server side with ASP.NET Identity. Then we create an Angular service to use this to authenticate a user and after that requests to Web API actions that use the Authorize attribute will be allowed.
There's still one problem. The logged in user will probably won't be allowed to access some pages of the app. Although using the app itself it won't be allowed, the HTML for the SPA is still available. If the user goes to http://website.com/app/views/notAllowedPage.html it will render in the browser. It's really not useful I know, but still I think to be a security failure, since the user shouldn't be allowed to get this HTML from the server.
Is there a way to secure this HTML or it is simply not possible?
We discussed the same problem in our developers group. Our conclusion was not to see this as a security thread.
What you want to protect is the data that is displayed, not the static "layout" of a HTML page. As long as the WebAPI services that deliver the data are secured and only allow authorized users to retrieve the data, we are safe.
Would that suit your needs as well?
We currently use this same setup.
Since we are using Angular, we don't do much with MVC itself or the Razor engine. The only things we are really doing with Razor is rendering a layout and the basic page (usually Index()).
So my recommendation is to do the same--instead of having a page website.com/app/views/notAllowedPage.html, have the user navigate to website.com/app/NotAllowed/, and secure the NotAllowedController with an Authorize attribute.
Create a service for securing your Angular htmlpage
As per the these services a guest user can't access the secure pages
Angular service
angular.module('application').factory('authorizationService', function ($resource, $q, $rootScope, $location,dataFactory) {
return {
permissionCheck: function (roleCollection) {
var deferred = $q.defer();
var parentPointer = this;
var user = dataFactory.getUsername();
var permission = dataFactory.getUserRole()
var isPermissionLoaded=dataFactory.getIsPermissionLoaded();
if (isPermissionLoaded) {
this.getPermission(permission, roleCollection, deferred);
}
else {
$location.path("/login");
}
return deferred.promise;
},
getPermission: function (permission, roleCollection, deferred) {
var ifPermissionPassed = false;
angular.forEach(roleCollection, function (i,role) {
switch (role) {
case roles.ROLE_USER:
angular.forEach(permission, function (perms) {
if (perms=="ROLE_USER") {
ifPermissionPassed = true;
}
});
break;
case roles.ROLE_ADMIN:
angular.forEach(permission, function (perms) {
if (perms=="ROLE_ADMIN") {
ifPermissionPassed = true;
}
});
break;
default:
ifPermissionPassed = false;
}
});
if (ifPermissionPassed==false) {
$location.path("/login");
$rootScope.$on('$locationChangeSuccess', function (next, current) {
deferred.resolve();
});
} else {
deferred.resolve();
}
},
isUserAuthorised: function () {
var isPermissionPassed = false;
var permission = dataFactory.getUserRole()
var isPermissionLoaded=dataFactory.getIsPermissionLoaded();
if (isPermissionLoaded) {
angular.forEach(permission, function (perms) {
if (perms=="ROLE_USER") {
ifPermissionPassed = true;
}
});
}
return isPermissionPassed;
}
};
});
and this code for your app.js where u mention your url and their controller
var application = angular.module('application', ['ngRoute']);
var roles = {
ROLE_USER: 0, //these are the roles which I want to secure from guest user
ROLE_ADMIN: 1
};
var routeForUnauthorizedAccess = '/login'; //if any unauthories user access the page the user will redirect on the login page
$routeProvider.when('/', {
templateUrl: 'view/home.html',
controller: 'homepage'
}).when('/login', {
templateUrl: 'view/loginpage.html',
controller: 'login'
}).when('/signup', {
templateUrl: 'view/signup.html',
controller: 'signup'
}).when('/dashboard', {
templateUrl: 'view/dashboard.html',
controller: 'dashboard',
resolve: {
permission: function(authorizationService, $route) {
return authorizationService.permissionCheck([roles.ROLE_USER, roles.ROLE_ADMIN]);
},
}
// the last dashboard page is access by only admin and the user who logged IN

Capture all $http premises with Angular

I would like to refresh the state of the user that is used in the navbar. but now I have to call .all(function() { refresh() }); on all $http.post premises.
Could I capture all of them with a configuration for instance?
Yes, you can intercept $http using request/response interceptors.
But a better design might be to set a $watch in the controller for the navigation bar on a property the POST calls change (possibly on $rootScope, though it is not recommended).
This is what I ended up doing:
app.config(function($httpProvider, $provide) {
// This sucks but Angular is so compliated that we can't get the rootScope
// in a simple way inside a factory for a configuration
function getRootScope() {
return angular.element('body').scope();
}
// Call refresh() after each HTTP destructive action
$provide.factory('refreshAfterRequests', function($q) {
return {
'response': function(response) {
// Every non-GET request can potentialy change the state of the user
if (response.config.method !== 'GET') {
getRootScope().refresh();
}
return response;
}
};
});
// The refresh is an interceptor on all HTTP requests made with Angular
$httpProvider.interceptors.push('refreshAfterRequests');
});
If someone have something better like how to get the scope from the inside the interceptor, I will gladly change this answer.

using angularjs http-auth with $resource

I am trying to make HTTP Auth Interceptor Module work together with $resource.
for now, i have a basic app working with services like this :
angular.module('myApp.services', ['ngResource']).
factory('User', function($resource){
return $resource('path/to/json/', {}, { 'login': { method: 'GET' } });
});
and then controllers like this one :
angular.module('myApp.controllers', []).
controller('User', ['$scope', 'User', function($scope, List)
{ $scope.user = User.query();
]);
and the app :
angular.module('myApp', ['myApp.services', 'myApp.directives', 'myApp.controllers']).
config(['$routeProvider', function($routeProvider) {
$routeProvider.when('/dashboard', {templateUrl: 'partials/dashboard.html', controller: 'Dashboard'});
$routeProvider.when('/user', {templateUrl: 'partials/user.html', controller: 'TrackingCtrl'});
$routeProvider.otherwise({redirectTo: '/dashboard'});
}]);
Until now, everything is working like expected. Then, i put an http auth on the server, so the json files http status are 401 and the browser display a login popup. I would like to use the HTTP Auth Interceptor Module to replace the browser login popup and handle the login. it is the purpose of this script, right ?
To do so, i am trying to understand the demo of the script and make that work with my app.
first, i injected 'http-auth-interceptor' to the app.
then, this was added to index.html
<body ng-controller="UserCtrl" class="auth-demo-application waiting-for-angular">
and the login form above ng-view:
<div id="login-holder">
<div id="loginbox">
<div id="login-inner" ng:controller="LoginController">
<form ng-submit="submit()">
Username: <input type="text" class="login-inp" ng-model="username"/><br/>
Password: <input type="password" class="login-inp" ng-model="password"/><br/>
<input type="submit" class="submit-login"/>
</form>
</div>
</div>
</div>
and the script at the bottom of the page:
<script src="lib/directives/http-auth-interceptor.js"></script>
in controller.js :
controller('LoginController', ['$scope', 'authService', 'User', function ($scope, authService, User) {
$scope.submit = function() {
User.login(
function(user, response) { // success
authService.loginConfirmed();
console.log('login confirmed');
}
);
}
}])
and this directive too :
directive('authDemoApplication', function() {
return {
restrict: 'C',
link: function(scope, elem, attrs) {
//once Angular is started, remove class:
elem.removeClass('waiting-for-angular');
var login = elem.find('#login-holder');
var main = elem.find('#content');
login.hide();
scope.$on('event:auth-loginRequired', function() {
login.slideDown('slow', function() {
main.hide();
});
});
scope.$on('event:auth-loginConfirmed', function() {
main.show();
login.slideUp();
});
}
}
})
Ok… that's it. but it's not working at all: the browser loggin form is still there. and that's the only way to login.
any idea on what i should do to make this work ?
From their webpage:
Typical use case:
somewhere the: $http(...).then(function(response) { do-something-with-response }) is invoked,
the response of that requests is a HTTP 401,
'http-auth-interceptor' captures the initial request and broadcasts 'event:auth-loginRequired',
your application intercepts this to e.g. show a login dialog (or whatever else),
once your application figures out the authentication is OK, you are to call: authService.loginConfirmed(),
your initial failed request will now be retried and finally, the do-something-with-response will fire.
So the only thing you need to do to interact with this is to have a parent scope to listen to even:auth-loginRequired. This can be done in a directive, in a controller, it doesn't matter. I haven't used the software, but I'm imagining something like this:
angular.module('myApp', [])
.service('api', function(httpAutenticationService, $rootScope, $location) {
$rootScope.$on('event:auth-loginRequired', function() {
// For the sake of this example, let's say we redirect the user to a login page
$location.path('/login')
})
$http.get(API_URL).then( ... ); // If the user is not authenticated, this will be intercepted (1)
})
.controller('LoginController', function(httpAutenticationService) {
// Let's imagine you have a route with the 'controller' option to this controller
$http.get(LOGIN_URL).then(function() {
httpAutenticationService.loginConfirmed(); // The success callback from (1) will be executed now
}, function(){
alert("Invalid credentials")
})
})

AngularJS service http success function using wrong "this" scope

The success function of a $http.put doesn't have access to the this scope of the service it's being called inside. I need to update a property of the service in the call back from the PUT request.
This is a cut down example of what I'm trying to do in a service:
var myApp = angular.module('myApp', function($routeProvider) {
// route provider stuff
}).service('CatalogueService', function($rootScope, $http) {
// create an array as part of my catalogue
this.items = [];
// make a call to get some data for the catalogue
this.add = function(id) {
$http.put(
$rootScope.apiURL,
{id:id}
).success(function(data,status,headers,config) {
// on success push the data to the catalogue
// when I try to access "this" - it treats it as the window
this.items.push(data);
}).success(function(data,status,headers,config) {
alert(data);
});
}
}
Sorry if there are some errors in the JS, the main point is how do I access the service scope from inside the success callback?
EDIT : while the answer to this question was correct, I switched to the factory method as both Josh and Mark recommended it
As far as I know, you can't. But I wouldn't try to run the service that way anyway. Here is a cleaner way:
.factory('CatalogueService', function($rootScope, $http) {
// We first define a private API for our service.
// Private vars.
var items = [];
// Private methods.
function add( id ) {
$http.put( $rootScope.apiURL, {id:id} )
.success(function(data,status,headers,config) { items.push(data); })
.then(function(response) { console.log(response.data); });
}
function store( obj ) {
// do stuff
}
function remove( obj ) {
// do stuff
}
// We now return a public API for our service.
return {
add: add,
store: store,
rm: remove
};
};
This is a very common pattern of developing services in AngularJS and it doesn't require any use of this in these cases.
Create a closure over a variable (often called that) that is assigned to this so that your callback functions will have access to your service object:
app.service('CatalogueService', function($rootScope, $http) {
var that = this;
...
).success(function(data,status,headers,config) {
that.items.push(data);
Here is a Plunker that uses $timeout instead of $http to demonstrate.

Resources