In a Rails application, a user has to be redirected to an external site for external processing. The controller generates a valid JSON string and a set of headers:
#result = HTTParty.post(
"https://some-test.supertester.com/checkout",
:verify => false,
:body => JSON.parse(#payment).to_json,
headers: {
'Content-Type' => 'application/json',
'X-TimeStamp' => "#{Time.now.to_i}",
'X-API-ID:' => "#{#api_id}",
'X-API-Signature:' => "#{#api_signature}"
},
timeout: 20
)
This action is launched by the user via:
<%= link_to t('proceed'), data_url_cart_path(id: #existing_cart.id), class: 'button' %>
whose controller action generates the above JSON string and call.
However, Rails tries to respond to this action with a view with the same action's name (even generating a blank with format.json { head :no_content }), when the goal is to redirect the user to the aforementioned URL with the defined headers and payload.
How can this be achieved?
HTTParty is making a network request with a json payload. In this situation #response will be the response from the network request. If you call puts #response.parsed_response.inspect you will see the json returned with said response in the terminal window running your Rails server.
I think you may not need to redirect the user to an external site at all, but instead check the response to see if the action you were trying to make was successful and handle it in your own application.
Related
I am trying to create an API for an application (both in Rails), but I have many doubts regarding authentication through an API.
Contextualizing a bit what I'm doing.
To create the API in Rails I'm using the rails-api and
devise_token_auth gems.
For the application that will consume the API's services I'm using
the httparty gem.
Now the question I have is with respect to login, the description in devise_token_auth about the sign_in path is:
Email authentication. Requires email and password as params. This route will return a JSON representation of the User model on successful login along with the access-token and client in the header of the response.
The problem is that I don't know how to handle the access-token, for example, if I execute this:
#result = HTTParty.post('url_of_my_api_on_heroku/auth/sign_in', :body => {"email": "someemail#example.com", "password": "ABCDEFGHI"}.to_json, :headers => { 'Content-Type' => 'application/json' })
On the application that will consume the API's services and I display the value of #result I can see the user data successfully... BUT I don't know how to get the access-token, nor use it.
I need some help, please.
This is how you get the access-token:
#result = HTTParty.post('url_of_my_api_on_heroku/auth/sign_in', :body => {"email": "someemail#example.com", "password": "ABCDEFGHI"}.to_json, :headers => { 'Content-Type' => 'application/json' })
#access_token = #result_login.headers["access-token"]
Now #access_token will have the current value of the access-token.
I'm having trouble making a cross-domain request from my shopify site to my rails app, which is installed as a shopify app. The problem, as stated in the title, is that my server warns me that it Can't verify CSRF token authenticity I'm making the request from a form returned by my rails app, which includes the relevant CSRF token. The request is done with jQuery's ajax method, and the preflight OPTIONS request is being handled by rack-cors.
I've included the X-CSRF-Token in my headers as was suggested in this answer. My post request is being made from a form, so my question is not answered here. The options request (mentioned in this question) is indeed being handled, as I just confirmed by asking this question. I've been stuck on this for a while, and have done a bit of reading.
I'm going to try walking through the process code-snippet by code-snippet, and maybe by the time I finish writing this post I will have discovered the answer to my problem (if that happens, then you won't ever get a chance to read this paragraph).
Here are the new and create methods from my controller.
class AustraliaPostApiConnectionsController < ApplicationController
# GET /australia_post_api_connections/new
# GET /australia_post_api_connections/new.json
def new
# initializing variables
respond_to do |format|
puts "---------------About to format--------------------"
format.html { render layout: false } # new.html.erb
format.json { render json: #australia_post_api_connection }
end
end
# POST /australia_post_api_connections
# POST /australia_post_api_connections.json
def create
#australia_post_api_connection = AustraliaPostApiConnection.new(params[:australia_post_api_connection])
respond_to do |format|
if #australia_post_api_connection.save
format.js { render layout: false }
else
format.js { render layout: false }
end
end
end
end
(I wonder about the respond_to block in the create method, but I don't think that would cause the CSRF token to fail verification.)
Within my app, at /AUSController/index, I have an ajaxified GET request that brings back the form from /AUSController/new. My goal is to be able to make all the same calls from a cross-domain origin as I can from within my app. Right now the GET request works for both, and so I will neglect to include the 'new' form. When the HTML is finally rendered, the form element has the following:
<form method="post" id="new_australia_post_api_connection" data-remote="true" class="new_australia_post_api_connection" action="http://localhost:3000/australia_post_api_connections" accept-charset="UTF-8">
<!-- a bunch more fields here -->
<div class="field hidden">
<input type="hidden" value="the_csrf_token" name="authenticity_token" id="tokentag">
</div>
</div>
</div>
</form>
The CSRF token is generated by a call to form_authenticity_token as detailed in one of the references mentioned above.
The next step is done differently in the two cases:
My app successfully returns the new form to the shop upon an ajax request. I've tested this within the app, that is by making an ajax call to /controller/new from /controller/index, and then submitting the form. This works like a charm. The js that is returned from a successful POST within my app is as follows:
/ this is rendered when someone hits "calculate" and whenever the country select changes
:plain
$("#shipping-prices").html("#{escape_javascript(render(:partial => 'calculations', :object => #australia_post_api_connection))}")
Which renders the following partial,
= form_tag "/shipping_calculations", :method => "get" do
= label_tag :shipping_type
%br
- #service_list.each_with_index do |service, index|
- checked = true if index == 0
= radio_button_tag(:shipping_type, service[:code], checked)
= label_tag(:"shipping_type_#{service[:code]}", service[:name])
= " -- $#{service[:price]}"
%br
When I call it from the same domain, request.header contains the following:
HTTP_X_CSRF_TOKEN
the_token_I_expect=
rack.session
{
"session_id"=>"db90f199f65554c70a6922d3bd2b7e61",
"return_to"=>"/",
"_csrf_token"=>"the_token_I_expect=",
"shopify"=>#<ShopifyAPI::Session:0x000000063083c8 #url="some-shop.myshopify.com", #token="some_token">
}
And the HTML is rendered and displayed nicely.
From the cross domain source, however, things are understandibly more complicated. This is where CORS and CSRF tokens and routes and all these little details start creeping in. In particular, when I make the ajax call I use the following script (which does not live in my rails app, it lives on the cross-domain server). The action of this ajax request is attached to the submit button by the callback function from the GET request, and I've included the GET request for the sake of completion.
<script>
var host = "http://localhost:3000/"
var action = "australia_post_api_connections"
console.log("start")
$.ajax({
url: host + action,
type: "GET",
data: { weight: 20 },
crossDomain: true,
xhrFields: {
withCredentials: true
},
success: function(data) {
console.log("success");
$('#shipping-calculator').html(data);
$('#new_australia_post_api_connection')
.attr("action", host + action);
$('.error').hide();
$(".actions > input").click(function() {
console.log("click")
// validate and process form here
$('.error').hide();
var to_postcode = $("input#australia_post_api_connection_to_postcode").val();
// client side validation
if (to_postcode === "") {
$("#postcode > .error").show();
$("input#australia_post_api_connection_to_postcode").focus();
return false;
}
tokentag = $('#tokentag').val()
var dataHash = {
to_postcode: to_postcode,
authenticity_token: tokentag // included based on an SO answer
}
// included based on an SO answer
$.ajaxSetup({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRF-TOKEN', tokentag);
}
});
$.ajax({
type: "POST",
url: host + action,
data: dataHash,
success: function(data) {
$('#shipping-prices').html(data);
}
}).fail(function() { console.log("fail") })
.always(function() { console.log("always") })
.complete(function() { console.log("complete") });
return false;
});
}
}).fail(function() { console.log("fail") })
.always(function() { console.log("always") })
.complete(function() { console.log("complete") });
$(function() {
});
</script>
However, when I call it from this remote location (the distant slopes of Shopify), I find the following in my request headers,
HTTP_X_CSRF_TOKEN
the_token_I_expect=
rack.session
{ }
And I receive a very unpleasant NetworkError: 500 Internal Server Error rather than the 200 OK! that I would like... On the server side we find the logs complaining that,
Started POST "/australia_post_api_connections" for 127.0.0.1 at 2013-01-08 19:20:25 -0800
Processing by AustraliaPostApiConnectionsController#create as */*
Parameters: {"weight"=>"20", "to_postcode"=>"3000", "from_postcode"=>"3000", "country_code"=>"AUS", "height"=>"16", "width"=>"16", "length"=>"16", "authenticity_token"=>"the_token_I_expect="}
WARNING: Can't verify CSRF token authenticity
Completed 500 Internal Server Error in 6350ms
AustraliaPostApiConnection::InvalidError (["From postcode can't be blank", "The following errors were returned by the Australia Post API", "Please enter Country code.", "Length can't be blank", "Length is not a number", "Height can't be blank", "Height is not a number", "Width can't be blank", "Width is not a number", "Weight can't be blank", "Weight is not a number"]):
app/models/australia_post_api_connection.rb:78:in `save'
The lack of a rack.session seems suspicious like the cause of my misery... but I haven't been able to find a satisfying answer.
Finally I have seen fit to include my rack-cors setup, in case it is useful.
# configuration for allowing some servers to access the aus api connection
config.middleware.use Rack::Cors do
allow do
origins 'some-shop.myshopify.com'
resource '/australia_post_api_connections',
:headers => ['Origin', 'Accept', 'Content-Type', 'X-CSRF-Token'],
:methods => [:get, :post]
end
end
Thank you so much for even reading all of this. I hope the answer has to do with that empty rack.session. That would be satisfying, at least.
Well one of my coworkers figured it out. The problem was, the has I was sending didn't have the same structure as the hash I was expecting in my controller.
In my controller I instantiate a new API connection as follows,
AustraliaPostApiConnection.new(params[:australia_post_api_connection])
I am looking for params[:australia_post_api_connection], but there is no such index in my data hash, which looks like,
var dataHash = {
to_postcode: to_postcode,
authenticity_token: tokentag // included based on an SO answer
}
To fix this I changed the JS file to contain,
var dataHash = {
to_postcode: to_postcode,
}
var params = {
australia_post_api_connection: dataHash,
authenticity_token: tokentag // included based on an SO answer
}
And now it works! Thanks co-worker!
I have a scenario where I'm doing a post in Sinatra via Typhoeus in app.rb. It looks like this:
post "/send-data" do
...
request = Typhoeus::Request.new("http://localhost:4000/renders",
:method => :post,
:headers => { :Accept => "text/html" },
:followlocation => true,
:timeout => 100, # milliseconds
:params => params )
# Run the request via Hydra.
hydra = Typhoeus::Hydra.new
hydra.queue(request)
hydra.run
...
end
When I post to 'send-data' Typhoeus successfully does it's post and pushes the user to the view of the created record (http://localhost:4000/renders/34634646464), which is a rails app.
The problem is that the user is never redirected away from /send-data, so if you refresh the page it tries to do the post again. I guess this makes sense, but I really need the user to be redirected to the final (url) location of the record. In other words, the new record can be seen, but this method of redirecting does not actually move the user off of the sinatra app.
What would be the best way to handle this? The only one I can think of off the top of my head is to not use 'followlocation', but rather have the /send-data controller action do the redirect after getting the response location fron Typhoeus.
I tried my suggestion and it works... and does not look too bad.
request = Typhoeus::Request.new("http://localhost:4000/renders.json",
:method => :post,
:headers => { :Accept => "json" },
:timeout => 100, # milliseconds
:params => params )
hydra = Typhoeus::Hydra.new
hydra.queue(request)
hydra.run
response = request.response
redirect response.headers_hash['Location']
I did have to make a change on my rails server. The rails create action responds with json and 'Location' is it's return value. 'Location'is the location of where the newly created record resides. Then I just do a Sinatra redirect which will redirect to the new record on the rails app.
I'm developping a post to a callback url in Ruby on Rails and use the Httparty library for this, I receive the post perfectly on the url but it seems that rails convert the data that is pushed to the url 2 times to parameters. Here is the code that I use to do the call :
#result = HTTParty.post("http://localhost:3000/mailchimp/callback/",
:body => {
:data => {
:title => 'This is the screen name'}
}.to_json,
:headers => { 'Content-Type' => 'application/json' } )
In the logs of the receiving application I got this :
Parameters: {"mailchimp"=>{"controller"=>"mailchimp", "action"=>"callback", "data"=>{"title"=>"This is the screen name"}}, "data"=>{"title"=>"This is the screen name"}}
You see directly that I have 2 times the data parameters, once in the controller hash and once in the normal parameters hash. How does this come?
This is caused by the ParamsWrapper module https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/params_wrapper.rb
This is enabled by default in your rails app by the initializer config/wrap_parameters.rb
I've been searching for hours now and haven't found anything that helps.
What I want to do:
I need to call the check_login-Method (as below), which needs parameters.
redirect_to check_login_users_url(
:user => {:name => input[1], :password => input [2] },
:stylesheet => 'scaffold',
:method => :get)
The point is that these params are sent in the method-call as in the "Redirected to"-line below.
Processing ApplicationController#execute(for 127.0.0.1 at 2009-12-19 00:28:40) [POST]
Parameters: {"command"=>{"line"=>"log dodo wg"}, "authenticity_token"=> <...token>}
Redirected to http://localhost:3000/benutzer/check_login?method=get&stylesheet=scaffold&user%5Bname%5D=dodo&user%5Bpassword%5D=wg
Completed in 9ms (DB: 0) | 302 Found [http://localhost/execute]
I want to prevent rails from putting the params into the url and pass them hidden instead.
When I send a form created with form_for, there's nothing in the url, so I assume it must be possible.
Please tell me how to do that.
Steps tried
I have tried different "html-verbs": get, put, post - no difference. Though the call of check_login is really short the url-with-params shows up in my Console
create an instance variable and pass it as param (strange, didn't work either)
watch form_for working – without results, got no clue
//edith:
Thanks for all your help so far. Perhaps I didn't specify my problem in enough detail.
I've got a text_field in which I enter short commands (experimentally). Its form calls execute in AppController, which in case of login-data performs redirect_to check_login. I don't need to access a webpage, I simply want to run the method. I liked the idea of putting it into :flash, but I'm wondering if there's a "neater" way to do pass the data hidden.
TL; DR Version: Use a form.
You're never going to be able to fully hide parameters, tools can be used to monitor requests and view the post data/parameters. You could however obfuscate it with an encrypted session. Also it appears that you're sending login info via a GET request, this is generally a bad practice.
That said...
What is going wrong for you is that you're not generating any post data with link_to :method => :post. link_to will use what ever parmas you give it to generate the url. Wheres forms will send all the params generated by the form as POST data to the url generated in the form_for call.
Upon receiving a POST request, Rails will merge parameters routing picks up from from the URL with the post data it receives into one params hash.
As in POST to
http://localhost:3000/benutzer/check_login?stylesheet=scaffold&user%5Bname%5D=dodo&user%5Bpassword%5D=wg
produces the same params hash in the receiving controller action as a POST to http://localhost:3000/benutzer/check_login with the following data:
stylesheet=scaffold&user[name]=dodo&user[pasword]=wg
There will be no distinction in the server log between the two requests.
If you look at what form_for is doing, it submits POST data built from the form inputs to the url generated by the arguments.
form_for #user, create_user_url(:stylesheet => "scaffold") do |f|
f.text_field :name
f.password_field, :password
end
This form will submit the form data to the url generated from the options. In this example the url is: http://localhost:3000/users/create?stylesheet=scaffold and the form data is:
user[name]=name_field_value_at_submit&user[password]=password_field_value_at_submit
link_to will not populate post data for you. You must either do it through a form or with javascript. The link_to documentation contains an example of doing this with javascript. Look for how the destroy with :onclick is handled.
If you really don't like buttons, you could use link_to_function to submit a form.
Replace
:method => :get)
with
:method => :post)
What's the difference between :get and :post? Read Methods GET and POST in HTML forms - what's the difference?
With form_for you create form which is then POSTed to server, that's why you don't see parameters in url - they're in http request body. But it is not possible to redirect user's browser from some action in controller to make another POST - if it would be possible, then I could redirect user to (for example) email change form of gmail or other forms. You can only redirect user to other site, which user's browser then GETs.
If you really don't want to show parameters in url, and both actions are in same application, then you can store those parameters in session or flash store, and retrieve in next request after redirect.
You can use Ajax request to send form data to action :
In some cases its not good to change :get into :post.
For instance in case of Controller's :index action its not good approach to use :post
So Use ajax call to submit form and update only dynamic content of the page.
In js.coffe script file
$ ->
$("#button-id").on "click", (ev) ->
$.ajax
type: "GET"
dataType: "html"
url: "/horoscope_dailies"
data:
date: date
success: (data) ->
$("#index_content").html data
error: (object, error) ->
console.log error
In your controller action
render partial: 'partial_name' if request.xhr?
In your view file:
%div{:id => 'partial_content'}
= render 'partial_name'