Testing scala Play (2.2.1) controllers with CSRF protection - scala

I've been having some problems testing controllers that use Play's CSRF protection. To demonstrate this, I've created a very simple Play application that minimally exhibits the problem.
https://github.com/adamnfish/csrftest
The full details are on the README of that repository, but to summarise here:
Consider a controller that is designed to handle a form submission. It has a GET method that uses CSRFAddToken and a POST method that uses CSRFCheck. The former adds a CSRF Token to the request so that a form field can be put in the rendered view, containing the valid token. When that form is submitted, if the CSRF check passes and the submission is valid, something else will happen (typically a redirect). If the form submission is not valid, the form submission is re-shown along with any errors so the user can correct the form and submit again.
This works great!
However, in the tests we now have some problems. To test the controller you can pass a fake request to it in the test. The CSRF check itself can be skipped by adding the nocheck header to the fake request but the view cannot be rendered because no token available to generate the form field. The test fails with a RuntimeException, "Missing CSRF Token (csrf.scala:51)".
Given that it works when it's actually running but not in the tests, it seems like this must be a problem with the way FakeRequests are run in Play tests but I may be doing something wrong. I've implemented the CSRF protection as described at http://www.playframework.com/documentation/2.2.1/ScalaCsrf and the testing as described at http://www.playframework.com/documentation/2.2.1/ScalaFunctionalTest. I'd appreciate any pointers if anyone has managed to test CSRF protected forms.

One solution is to test using a browser, eg Fluentlenium, as this will manage cookies etc, so the CSRF protection should all just work.
The other solution is to add a session to the FakeRequest so that it contains a token, eg:
FakeRequest().withSession("csrfToken" -> CSRF.SignedTokenProvider.generateToken)
Obviously if you're doing that a lot, you can create a help method to do that for you.

Bonus answer for those interested in Java: I got this to work in the Java version of Play Framework 2.2 by adding
.withSession(CSRF.TokenName(), CSRFFilter.apply$default$5().generateToken())
to fakeRequest()

Following on from #plade, I added a helper method to my base test class:
protected static FakeRequest csrfRequest(String method, String url) {
String token = CSRFFilter.apply$default$5().generateToken();
return fakeRequest(method, url + "?csrfToken=" + token)
.withSession(CSRF.TokenName(), token);
}

To those that are still interested: I managed to solve this problem globally by enabling CSRF protection in tests. The app will then create a token for every request that does not contain one. See my answer to this question

For those who might be interested, I created a trait for play 2.5.x :
https://stackoverflow.com/a/40259536/3894835
You can then use it in your tests requests like the addToken{} of the controller :
val fakeRequest = addToken(FakeRequest(/* params */))

I use the following method in my base integration test class:
def csrfRequest(method: String, uri: String)(implicit app: Application): FakeRequest[AnyContentAsEmpty.type] = {
val tokenProvider: TokenProvider = app.injector.instanceOf[TokenProvider]
val csrfTags = Map(Token.NameRequestTag -> "csrfToken", Token.RequestTag -> tokenProvider.generateToken)
FakeRequest(method, uri, FakeHeaders(), AnyContentAsEmpty, tags = csrfTags)
}
Then you can use it in your tests where you would use FakeRequest.

Related

How do I handle the need for CSFR token when using SAP Cloud SDK?

I am using the SAP Cloud SDK for Java to do CRUD on the SalesOrder APIs in S/4. Everything works well in that I can carry out these actions from Postman. However, these requests from Postman only work if I include a pre-request script to get a csrf token as outlined in this blog post
If I run the requests without the pre-request script outlined in the blog post, I get a '403 Forbidden'. As I said it works from Postman, but I would like to understand how this should be handled without the need for this script, for example if I was making a request from another application. Does the SDK allow me to handle this from the application code somehow. Maybe I am missing something.
Thanks for your time.
EDIT:
I am not making requests to the S/4 directly from Postman. I have an app deployed which is using the Cloud SDK to make the requests to S/4. It works if I use the pre-request script to fetch the CSFR token and attach it to the request before I send it, but 403 if I don't. So, if we imagine I am not using Postman but some ui somewhere to fill a form and send this request my understanding is that I shouldn't, as you suggested, have to worry about this token, that my service in the middle which uses the SDK and the VDM should handle this for me. This is what I am struggling to understand.
This is the servlet code:
#Override
protected void doPost(final HttpServletRequest request, final HttpServletResponse response)
throws ServletException, IOException {
String body = IOUtils.toString(request.getReader());
JSONObject so = new JSONObject(body);
String distributionChannel = so.get("DistributionChannel").toString();
String salesOrderType = so.get("SalesOrderType").toString();
String salesOrganization = so.get("SalesOrganization").toString();
String soldToParty = so.get("SoldToParty").toString();
String organizationDivision = so.get("OrganizationDivision").toString();
String material = so.get("Material").toString();
String requestedQuantityUnit = so.get("RequestedQuantityUnit").toString();
SalesOrderItem salesOrderItem = SalesOrderItem.builder()
.material(material)
.requestedQuantityUnit(requestedQuantityUnit).build();
SalesOrder salesOrder = SalesOrder.builder()
.salesOrderType(salesOrderType)
.distributionChannel(distributionChannel)
.salesOrganization(salesOrganization)
.soldToParty(soldToParty)
.organizationDivision(organizationDivision)
.item(salesOrderItem)
.build();
try {
final ErpHttpDestination destination = DestinationAccessor.getDestination(DESTINATION_NAME).asHttp()
.decorate(DefaultErpHttpDestination::new);
final SalesOrder storedSalesOrder = new CreateSalesOrderCommand(destination, new DefaultSalesOrderService(),
salesOrder).execute();
response.setStatus(HttpServletResponse.SC_CREATED);
response.setContentType("application/json");
response.getWriter().write(new Gson().toJson(storedSalesOrder));
logger.info("Succeeded to CREATE {} sales order", storedSalesOrder);
} catch (final Exception e) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
logger.error(e.getMessage(), e);
logger.error("Failed to CREATE sales order", e);
}
}
And the CreateSalesOrder command:
public SalesOrder execute() {
return ResilienceDecorator.executeSupplier(this::run, myResilienceConfig);
}
protected SalesOrder run() {
try {
return salesOrderService.createSalesOrder(salesOrder).execute(destination);
} catch (final ODataException e) {
throw new ResilienceRuntimeException(e);
}
}
I am using the version 3.16.1 of the SDK and have set logging level to DEBUG for the SDK in the manifest:
SET_LOGGING_LEVEL: '{ROOT: INFO, com.sap.cloud.sdk: DEBUG}'
and logging level to DEBUG in logback
If I remove the pre-request script from the request and send it I get the 403 response and logs shows the following messages:
"logger":"com.sap.cloud.sdk.service.prov.api.security.AuthorizationListener","thread":"http-nio-0.0.0.0-8080-exec-4","level":"DEBUG","categories":[],"msg":"Reading
user principal"
"logger":"com.sap.cloud.sdk.service.prov.api.security.AuthorizationListener","thread":"http-nio-0.0.0.0-8080-exec-4","level":"DEBUG","categories":[],"msg":"Destroying Authorization as it is end of request." }
"logger":"com.sap.cloud.sdk.service.prov.api.security.AuthorizationService","thread":"http-nio-0.0.0.0-8080-exec-4","level":"DEBUG","categories":[],"msg":"Destroying Authorization JWT Token." }
As the other answers focus on the app to S/4 communication and you adjusted your question to make clear that you mean the User (e.g. Postman) to app communication I'll provide some additional information.
As mentioned by the other answers the CSRF handling to the S/4 system (or any OData endpoint) is automatically handled on side of the OData VDM.
What you are now encountering is the secure default configuration of the SAP Cloud SDK Maven Archetypes, which have the RestCsrfPreventionFilter activated by default.
This filter automatically protects all non-GET endpoints from CSRF by requiring you to fetch a CSRF Token prior to your request which you then provide.
This is completely unrelated to the OData VDM call to the S/4 system in the background.
To remedy your problems there are now three next steps:
Use a GET endpoint instead of POST
Probably only as a temporary workaround
Remove the RestCsrfPreventionFilter temporarily from your web.xml
This should not be done for productive uses, but might make your life in the initial evaluation easier.
"Live with it"
As this is a commonly used pattern to protect your application against CSRF it's advised to keep the filter in place and do the CSRF-Token "flow" as required.
Further Reading
OWASP description of CSRF: https://owasp.org/www-community/attacks/csrf
OWASP cheat sheet on CSRF protection (linked to the approach used by the filter): https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers
JavaDoc of the RestCsrfPreventionFilter: https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/filters/RestCsrfPreventionFilter.html
Inkers
You're correct, with an API tool like Postman you have to make a HEAD request first to get a CSRF token.
However, in Cloud SDK for Java, we take care of getting and refreshing CSRF token for you when you're making any CRUD request.
Here's an example of reading a Saler Oder item and updating it afterward:
// Create a new sales order item
SalesOrderItem item = new SalesOrderItem();
item.setSalesOrder(SALES_ORDER);
item.setNetAmount(new BigDecimal(NET_VALUE));
item = service.createSalesOrderItem(item).execute(destination).getResponseEntity().get();
// Modify it with a PATCH update to 9000 net value
item.setNetAmount(new BigDecimal(NET_VALUE_UPDATED));
ModificationResponse<SalesOrderItem> response = service.updateSalesOrderItem(item).modifyingEntity().execute(destination);
Try it and let up know if it works fine for you. We're happy to assist if you'll encounter any difficulties.
The SDK makes an attempt to fetch a CSRF token automatically within execute(destination). This happens before issuing the actual request. If the attempt is successful the token will be included in the request. If not, the request will be send regardless.
Please increase the log level to debug for all com.sap.cloud.sdk packages if you think this is not happening correctly. Also it would be great to see the actual HTTP requests that go in and out which you can enable by setting the log level of org.apache.http.wire also to debug. Then attach the stack trace here together with the SDK version you are using and the exact code you are invoking.

handling the The CSRF token in symfony's forms when in public REST context

I'm developer my first symfony (3) app. it is a REST service publicly accessible.
I'm doing this using FOSRestBundle.
I'll have to ad some admin forms soon or later, and I'll probably want to create them directly (without passing by the extra work of consuming my own web services)
I wonder how to handle the CSRF token in this case. I see different solutions:
globally deactivate the CSRF token : I don't want to do this
create two set of forms, one with the token activated : form my admin forms, the other one for the REST API. => in this case, the rest API can't have a fallback _format=html
find a way to give the api consumer an auth, with an API_GROUP, and disable the token for this group
it seem to me the best solution, but I don't know how to do it transparently, without affecting the auth of my future admin, and without needing to give credentials in the REST request.
use an event listener in order to hack symfony's auth mechanism and give an auth if a call is made to the REST API (all but _format=html)
Which one of this (or other) solution seem the best to you, and how would you code it?
I found a way, perhaps not the best one, but it works :
$_format = $request->attributes->get('_format');
if ('html' == $_format) {
$form = $this->createForm(ItopInstanceUserType::class, $itopInstanceUser);
} else {
$form = $this->createForm(ItopInstanceUserType::class, $itopInstanceUser, ['csrf_protection' => false]);
}
For me, forget CSRF token managed by yourself, check subjects like Oauth authentication.
Take a look here: https://github.com/FriendsOfSymfony/FOSOAuthServerBundle/blob/master/Resources/doc/index.md
FOSOAuthServerBundle works perfectly with FOSRestBundle.

Play Framework: Don't change URL after form validation failed

In a plain Play application I have the following scenario.
A route file which looks like this:
GET /accounts/add controllers.Accounts.add()
POST /accounts controllers.Accounts.create()
The first route results in a view where I can add a new account. The form to submit the new account looks something like this:
#helper.form(action = routes.Accounts.create()) {...}
Now the controller binds the input to the form and checks for any validation errors:
public static Result create() {
Form<Account> form = Form.form(Account.class).bindFromRequest();
if (form.hasErrors()) {
return badRequest(views.html.account.add.render(form));
}
...
}
Now the thing is, the client will see the same view with some additional error messages. However, meanwhile the URL has changed from http://example.com/accounts/add to http://example.com/accounts.
If the client now reloads the browser this calls GET http://example.com/accounts (which isn't even mapped in this scenario - thus getting a 404 - Not Found).
Maybe it's just me but I find this kind of annoying and browsing some GitHub projects I couldn't find a good solution for this case.
Of cause things would be much simpler if the second route is rewritten to:
POST /accounts/add controllers.Accounts.create()
... in which case everything works fine. But from a REST point of view this doesn't feel good either. The same applies to update scenarios (having GET /accounts/:id/update vs. PUT /accounts/:id).
Is there a guide on how to handle this? Am I getting something wrong or is this no problem at all (from a pragmatic point of view)?
It's not possible to leave the previous URL because a request for a new address has already been made. A controller only provides response for a requested resource. To go to the previous URL you could only make a redirect in case of validation failure but you would lost errors that way so this is not a solution.
I suggest mapping both actions with the same URL. This way you would solve problem with the browser reload.
If you create a REST service for http clients that aren't browsers you will probably want to serve different response than a simple http page. The separation of actions for particular clients could be a good solution for keeping REST API clean and a browser user happy.

How to follow redirects inside test cases?

Based on: Testing a page that is loaded after a redirect
Play!2.0 Framework (Scala) does have API which we can use to make redirects. However is there any way in Play Framework ( Scala ) that the test case automatically follows redirect responses. I would not want to repeat the redirect logic every time.
How to follow redirects inside test cases for Play Framework ( Scala ) ?
Edit: To make it completely clear. There isn't a built-in way to do this, and for a good reason. Unit tests are supposed to be simple and isolated. A single unit test is meant to test a single piece of functionality.
Example: You make a GET request to a page that needs authentication, but you provide no credentials, so the server responds with SEE OTHER and a flash message attached. You then make sure it responded to that request appropriately. Test over. The controller function that responds with SEE OTHER should have no knowledge of the other, other than maybe it'll do something with that flash data.
Example 2: You make a GET request to a login page with flash data attached. The server responds with OK and an HTML view displaying the flash message within it. You make sure it does so.
If you really want to tie these two together, then perhaps tests using WithBrowser would suit you more.
If you're testing redirect information, it's better to isolate it from whatever controller function/view the redirect is pointing to. There are test helpers you can use to achieve this.
Let's say you want to test
Redirect("/login").flashing("message" -> "You are not logged in.")
In your test you could use these helpers:
import play.api.test._
val request = FakeRequest(GET, "/secretpage")
val Some(result) = route(request)
status(result) must equalTo(SEE_OTHER)
redirectLocation(result) must beSome("/login")
flash(result).get("message") must beSome("You are not logged in.")
Note that this is for Play 2.1.x and above. If you're using 2.0.x, you should change route to routeAndCall.

Display authenticated user info in header with secure social and play mvc

I am currently working on authentication with play 2.x and securesocial plugin with scala.
My website should always have user info like "Username" "avatar image" etc in the header all the times unless user logout.
Now using secure social I am trying to get the current user of the request and made it implicit so that I can get the value for all the requests and avoid passing it as params to the template.
implicit def user(implicit request: RequestHeader) = SecureSocial.currentUser;
But the securesocial.core.SecureSocial requires a implicit request which I can't uness I have Action {} in my controller. Using implicit is very convenient but i find securesocial stuff kind of confusing.
The request object you receive in SecuredActions has a user already: request.user will give you the current one. If you pass this instance to your views you will have the user available. See SecuredRequest in SecureSocial.scala.
Try UserAwareAction instead of SecuredActions if you don't want a redirect