I am implementing a payment gateway in my app.
Its like this:
The user fills the form with necessary details, along with a field containing return_url(say http://myapp.com/ebs_payment/ebs_response?dr={somedata}) and submit the form to a secure payment site. After the transaction is complete, the secure site puts some encrypted data into my param {dr} and the user is redirected back to the return url. The problem here is, when the user returns to the app with the return_url, the application fails to pick up the session data and returns a nil value.
Before submitting the form, I put the object #fdPaymentDets in to session.
Here is my controller:
class EbsPaymentController < ApplicationController
#before_filter :login_required,:check_user_authenticate
#access_control [:ebs_response] => ('user')
def ebs_response
#fdPaymentDets = session["fd_payment_details"]
#deal = Deal.find(#fdPaymentDets.deal_id)
#categories = find_all_categories
end
private
def find_all_categories
#sp = SitePreference.find(:first)
Category.find(:all,:limit => #sp.categories_display_limit,:order => 'position')
end
end
When the user is redirected to the return url (http://myapp.com/ebs_payment/ebs_response?dr={encrypted_data}) from the secure site, rails is not picking the #fdPaymentDets object from session and making it nil thus resulting in an error when accessing data from the nil object.
The surprising thing is that, when I put the same return_url in my browser by hand, the session data is neatly picked and everything goes well.
Am missing any thing here? What could be the obvious reason?
Any help appreciated.
Cookies and redirects are messy and bug prone (from a browser's implementation perspective).
Take a look at
Safari doesn't set Cookie but IE / FF does
Suggestion would be to change the implementation to set the session first in the show action, and then update the value before the redirect
Related
I've got a number of security concerns about my current application and wondering if I am leaving myself open to abuse, in the following arenas.
a) .My main access control method is by maining a current_user, current_company current_project method in my application controller. These methods return object based on stored session keys established when a user logs in and cleared when they log out. I.e if I want to know something about the current user, I can call "current_user.role" or if I want see whether the account a user is trying to change belongs to him, I check whether the associated account id which is requested in the url actually belongs to that user, essentially as follows
in Account controller
def account_info
redirect_to login_path if !user.logged_in
account_id=params[:account_id]
#account = Account.find(account_id)
unless account_belongs_to_user(account_id)
redirect_to unauthorized_path
end
end
In my application controller, when a user is initially authenticated, I do something like this:
session[:current_user_id] = user.id
and clear that session key when the user logs out.
Then when account is requested, and account_belongs_to_user is called, the application controller processes it, more or less like this:
def account_belongs_to_user(account_id)
account = Account.find(account_id)
return account.user_id==session[:current_user_id]
end
So I guess my security scheme ultimately relies on whether the session data is secure and not trivially spoofable.
b) When I render pages I sometimes pass objects which have senstive data to my erb pages to generate the page text.
For example, I might pass a "company" object (ActiveRecord) to the view to generate an invoice screen. But the company object, passed as #company, has a lot of sensitive data like access keys and the like. Not really being fully aware of the the internals, if I don't specifically include something like:
<%= #company.access_token %>
on my web page, can I be confident that the attributes of #company won't somehow be passed into the browser unless I specifically ask for them to be rendered on the page?
This is obviously an issue when using rails to serve data for say, AngularJS single page applications, as everything I pass for Angular to render the page I assume is probably accessible to an evil-doer even if not on the page itself, but I'm hoping that's not the case with pages generated server side by rails.
This may be a naive question, but thanks as I just want to be certain what I am doing before start spilling secrets all over the place.
put an authentication for the token using active_record callback
https://guides.rubyonrails.org/active_record_callbacks.html
My Rails app depends on the Rails cache to temporarily hold user input and pass it between controller actions during the user log in process. However, I realized that I have made a pretty serious error (since I am super-new to Rails and MVC, in general) and could use some help/advice/wisdom - basically, if two users are simultaneously (or nearly simultaneously) submitting data and going through the controller steps below, there's going to be some trouble - lost data, one user's data being entered as another, etc. When I was building this, I did not understand the nature of controllers, though I for sure have a better idea now....
In short, here's how the code below is intended to work: a non-logged-in user fills out a form and submits that data which is passed to the submission action and held in a Rails cache object called incoming_report; the user is then prompted to log in (via Devise), and a successful log in triggers the approval controller action which reads the incoming_report cache object, saves that data to the database, and then creates a new cache object called ids to hang on to the id #'s of the user's entries; this redirects to the summary action which gathers up those ids and shows the corresponding data to the user as a summary of their entry.
report_controller.rb
#ON SUBMITTING THE FORM...
def submission
#incomingReport = ActiveSupport::JSON.decode(params[:report])
#SUBMITTED DATA IS HELD IN THE CACHE AS 'incoming_report' TO PASS TO approval CONTROLLER ACTION ON SUCCESSFUL LOGIN
Rails.cache.write("incoming_report",#incomingReport)
end
#ON SUCCESSFUL LOG IN...
def approval
#incomingReport = Rails.cache.read("incoming_report")
#newReportIDArray = Array.new
#incomingReport.each do |x|
#DATA PROCESSING STUFF HERE...
end
#DELETE 'incoming_report' FROM CACHE
Rails.cache.delete("incoming_report")
#ID NUMBERS OF ENTRIES ARE HELD TO PASS TO summary CONTROLLER ACTION
Rails.cache.write("ids",#newReportIDArray)
redirect_to report_summary_path
end
#AFTER DATA IS ENTERED...
def summary
#newReportIDs = Rails.cache.read("ids")
#DELETE 'ids' FROM CACHE
Rails.cache.delete("ids")
end
For the most part, this works very well, but if users cross each other during any of these actions, it's disastrous. For instance, if one user hits the submission action while another user is busy logging in, the incoming_report object of the second user will overwrite that of the first user... and so on and so forth.
The main source of this problem is the fact that users log in after they have clicked the form submit button - the app must be structured that way - and the data can't be held in local/session storage objects because it includes base64 strings and is too long to employ those methods in certain browsers. Holding this data on the server seemed like the best course of action until I noticed this issue...
Is there any way that I can prevent users from getting in each other's way in the controllers? Or have I made a pretty fatal error?
Editing to show solution
Ultimately, using session variables instead of the Rails cache saved the day. Here's the reconfigured controller to show the solution...
def submission
#incomingReport = ActiveSupport::JSON.decode(params[:report])
#newReportIDArray = Array.new
#incomingReport.each do |x|
#DATA PROCESSING STUFF HERE
#newReportIDArray.push(#new_report.id)
end
session[:ids] = #newReportIDArray
respond_to do |format|
format.json do
render json: {
success: 200
}.to_json
end
end
end
def approval
#reportIDs = session[:ids]
#reportIDs.each do |x|
#new_report = Report.find(x)
#new_report.user_id = current_user.id
#new_report.save
end
redirect_to report_summary_path
end
def summary
#reportIDs = session[:ids]
end
Many thanks to the two folks who helped me on this and showed me the ways of the session variable.
I ran into a problem with the session variable being 'lost' between controller actions - Rails / Devise - updating session variables between controller actions
I would suggest that you store the initial information into the database. From there I would either save the id in a cookie or session variable. You would then be able to retrieve the information from the database in the approval controller. Another option I would consider would be to generate a guid as an id and pass that along to the sign in page as a query string parameter. Store the guid in a hidden input tag, under the form tag. Then when the user submits their credentials, you can also pass the guid. At that point you log them in. If successful, pull the guid from the posted data and then retrieve the database information with the guid.
Wade
Without removing this function at all.
I have form input, but rather than rendering view on validation fail without redirect, I redirect user to new page, but store his input in Redis, so don't loose errors messages and his actual entries:
def create_list
result=super
if result[:all_records_saved]
redirect_to program_stream_path(#stream.program, #stream)
else
redirect_to new_students_list_stream_path(#stream, :invalid_entry_id=>result[:id])
end
end
Where I pass invalid entry to method redis_store (which adds redis session)
def redis_store(invalid_entries)
if $redis.exists("invalid_entries_ids")
id = $redis.incr("invalid_entries_ids")
else
$redis.set("invalid_entries_ids", 0, ex: 600)
id = 0
end
invalid_entries.each do |entry|
$redis.multi do
$redis.rpush("invalid_entries_attr:#{id}", entry[0])
$redis.rpush("invalid_entries_errors:#{id}", entry[1])
$redis.expire("invalid_entries_attr:#{id}", 600)
$redis.expire("invalid_entries_errors:#{id}", 600)
end
end
id
end
So user is redirected back, with params[:invalid_entry_id], then other method fetches data from redis using this id.
Issue
I just understood, that if someone gets this redirect:
/edit-students-list?invalid_entry_id=1
He can change it to
/edit-students-list?invalid_entry_id=0
Or any id smaller than he actually got, and access to data he didn't meant to have access to.
That is not something big, but I would like to avoid it without removing all effort put to get this function working.
I notice that Rails flash doesn't need any params or external pointer to find out which flash belongs to which request. How I can implement this in my redis store/get functions? Or any other solution?
In Rails, I have implemented the below code for user auth (confirmed to be correct). However, I wanted to confirm my thinking for this strange session[:session_token]. is this the "cookie" that is stored in the browser?
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :current_user, :signed_in?
private
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
def signed_in?
!!current_user
end
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
def sign_out
current_user.try(:reset_token!)
session[:session_token] = nil
end
def require_signed_in!
redirect_to new_session_url unless signed_in?
end
end
My understanding so far of how this works is that whenever the browser/client sends a request to rails, the cookie (with the session[:session_token]) is also sent over, thus allowing the current_user method to find the user. Is my understanding correct? This is strange to me because there's a gap of knowledge of how exactly the browser/client gets access to the session cookie when we declare it in ApplicationController (Rails-side).
You are pretty much there. Although, I have a feeling you might be confusing apples with oranges...
Sessions:
Very often in dynamic web sites one would want to store user data between HTTP requests (because http is stateless and you can't otherwise associate a request to any other request), but you don't want that data to be readable and/or editable on the client-side inside of the URL (like.. yourwebsite.com/yourPage?cookie=12345&id=678), and so on..., because you don't want the client to play around with that data without passing through your server-side code.
One way to solve this problem is to store that data server-side, give it a "session_token"(as you called it), and let the client only know (and pass back at every http request) that token. This is how the session is implemented.
Cookies:
The most common technique for implementing sessions in Rails involve using cookies, which are small pieces of text placed on the user’s browser. Because cookies persist from one page to the next, they can store information (such as a session_token or whatever else you want) that can be used by the application to retrieve the logged-in user from the database.
Where is the Session Stored in Rails?
Using both of the above concepts I can now tell you that the default session store inside of Rails is CookieStore, which is about 4KB in size.
To put it simply...
def sign_in(user)
#current_user = user
session[:session_token] = user.reset_token!
end
...method that you defined places the user into a temporary session.
Then the idea is that the following...
def current_user
#current_user ||= User.find_by_session_token(session[:session_token])
end
...method would find and retrieve the user from the database corresponding to the session token and initialize it to a variable you specified.
Additional info:
You should also note that there is an important difference between Rails's session and cookies helper methods...
They both generate cookies, however, session[...] method generates temporary cookies, which should expire upon the browser exit, and cookies[...] method creates persistent cookies, which do not.
Additionally, I would suggest having a look at Section 2 of Ruby on Rails Security guide. You might find it useful.
Hope this helps you out.
Session is stored in server side. And,
Cookie is stored in client side (in browser cookie). And,
When client/browser send a request to rails server, every time cookies are sent to rails server.
When a session is set in rails server, like: session[:user_id] = 4,
Rails store it in server side.
Session is saved in server side like key value pair (like json object)
For each browser, Rails set a session identifier in cookie, so that, Rails can find the correct session information for a request.
Without session identifier in cookie, Rails do not know, what session belongs to what browser.
So, session will not work without cookie.
Edit: Explain: sessions are stored server side
Suppose, I am using your web application, and after login I will be redirected to home page.
I open login page, input username and password, and click login button.
The form is submitted to sessions#login action.
in sessions#login - you check username and password - and set session[:session_token]:
if username and password is correct
random_unique_identifier_string = #user.remember_token
session[:session_token] = random_unique_identifier_string
redirect_to root_url
end
When server run this code session[:session_token], server need an unique identifier for each browser session.
So, server generate an unique identifier for this browser, such as: abc123
Server set all session variables in a place (may be in some folder or in database), label this folder as abc123.
Now server send a cookie request to browser - to set cookie _ebook_session = abc123.
(I see, if my app name is ebook, in rails, cookie name is like: _ebook_session)
Now the page redirect to home page.
** Note: Everything above happen in single request **
Now, in my browser, I want to open some page that need authentication (suppose, dashboard page).
You added before_action: require_signed_in! in dashboard controller.
So, when I open dashboard page in my browser, browser by default send all cookies with every request. so _ebook_session cookie is sent to server. Your server gets the value of _ebook_session cookie is abc123. Now your application know we need to look in abc123 folder for session. Now you can get value of session[:session_token] from abc123 folder.
** I have explained second request above **
Each browser needs unique session identifier.
Important: _ebook_session cookie will be set in browser in first request. If we already have _ebook_session cookie set in a browser, we do not need to set it again, second, third and next requests in that specific browser.
I hope, you understand.
I read the few posts about troubleshooting stored_location_for here, but can't seem to figure it out and not sure how to troubleshoot.
I tried deleting my custom after_sign_in_path_for, but that didn't work either. My location is never getting saved, although as I understand it after each session/page update it should store the location. Do I need to through that in as a filter manually?
def after_sign_in_path_for(resource)
stored_location_for(resource) ||
if resource.is_a?(Account)
add_quote_to_account(resource)
if resource.applications.any?
edit_application_path(resource.applications(true).last)
else
root_path
end
else
super
end
end
May be you are not storing the location where you want to redirect_to after signing in with devise. Devise provides two methods - store_location_for and stored_location_for
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/store_location.rb
Suppose your user model is named "user" then
A call to store_location_for(:user, my_desired_path) in your controller will store the url "my_desired_path" in session with key "user_return_to". Basically this method will simply do this - session["user_return_to"] = my_desired_path. Probably you are missing this. I have a booking controller and a "login" action which stores the checkout location for booking in booking controller and then displays a login form for users in rendered view -
def login
my_desired_path = url_for(controller: 'bookings', action: 'checkout')
store_location_for(:user, my_desired_path)
end
Now you can use stored_location_for(:user) to retrieve my_desired_path from your session. So to say, a call to stored_location_for(:user) will return "my_desired_path.
Now if you use stored_location_for in your custom after_sign_in_path_for(:user) then it shall return "my_desired_path".
Additional Point -
A call to stored_location_for(:user) returns session[:user_return_to] but also clears this session after returning the value if your redirect format is navigational format. So a second call to stored_location_for(:user) will not return my_desired_path. And sometimes this is how we want our application to behave. On contrary, if your redirect format is not navigational format then session wont be cleared and a second sign-in will again redirect to same "my_desired_path".
Sometimes you want to redirect to different locations in signing-in from different pages in your application. Suppose, you want to redirect to "\X" on page A and to "\Y" on page B. Now follow these steps -
User in on page A - store_location_for(:user, "\X") is saved in session.
Application provides a sign-in form but User does not sign-in and just browse here and there in your application.
User is on page B and perform a sign-in expecting to land on "\Y" but unexpectedly lands to "\X".
So take care of it in your application. This scenario is more likely to occur in applications which uses AJAX for signing-in.